@laravel/echo-vue
Version:
Vue hooks for seamless integration with Laravel Echo.
474 lines (404 loc) • 11.7 kB
text/typescript
import { type BroadcastDriver, type ConnectionStatus } from "laravel-echo";
import { onMounted, onUnmounted, ref, watch, type Ref } from "vue";
import { echo } from "../config";
import type {
BroadcastNotification,
Channel,
ChannelData,
ChannelReturnType,
Connection,
EventName,
InferEventPayload,
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);
}
};
// Overload for automatic type inference from event name
export function useEcho<
TEvent extends EventName = EventName,
TDriver extends BroadcastDriver = BroadcastDriver,
TVisibility extends Channel["visibility"] = "private",
>(
channelName: string,
event: TEvent,
callback: (payload: InferEventPayload<TEvent>) => void,
dependencies?: any[],
visibility?: TVisibility,
): {
leaveChannel: (leaveAll?: boolean) => void;
leave: () => void;
stopListening: () => void;
listen: () => void;
channel: () => ChannelReturnType<TDriver, TVisibility>;
};
// Overload for multiple events with automatic type inference
export function useEcho<
TEvent extends EventName = EventName,
TDriver extends BroadcastDriver = BroadcastDriver,
TVisibility extends Channel["visibility"] = "private",
>(
channelName: string,
event: TEvent[],
callback: (payload: InferEventPayload<TEvent>) => void,
dependencies?: any[],
visibility?: TVisibility,
): {
leaveChannel: (leaveAll?: boolean) => void;
leave: () => void;
stopListening: () => void;
listen: () => void;
channel: () => ChannelReturnType<TDriver, TVisibility>;
};
// Overload for explicit payload type (backward compatibility)
export function useEcho<
TPayload,
TDriver extends BroadcastDriver = BroadcastDriver,
TVisibility extends Channel["visibility"] = "private",
>(
channelName: string,
event: string | string[],
callback: (payload: TPayload) => void,
dependencies?: any[],
visibility?: TVisibility,
): {
leaveChannel: (leaveAll?: boolean) => void;
leave: () => void;
stopListening: () => void;
listen: () => void;
channel: () => ChannelReturnType<TDriver, TVisibility>;
};
// Implementation
export function 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 cb = (notification: BroadcastNotification<TPayload>) => {
if (!listening.value) {
return;
}
if (events.length === 0 || events.includes(notification.type)) {
callback(notification);
}
};
const listen = () => {
if (listening.value) {
return;
}
result.channel().notification(cb);
listening.value = true;
};
const stopListening = () => {
if (!listening.value) {
return;
}
result.channel().stopListeningForNotification(cb);
listening.value = false;
};
onMounted(() => {
listen();
});
onUnmounted(() => {
stopListening();
});
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 function useChannel<
TDriver extends BroadcastDriver = BroadcastDriver,
TVisibility extends Channel["visibility"] = "private",
>(channelName: string, visibility: TVisibility = "private" as TVisibility) {
const channel: Channel = {
name: channelName,
id: ["private", "presence"].includes(visibility)
? `${visibility}-${channelName}`
: channelName,
visibility,
};
const subscription: Connection<TDriver> =
resolveChannelSubscription<TDriver>(channel);
const tearDown = (leaveAll: boolean = false) => {
leaveChannel(channel, leaveAll);
};
onUnmounted(() => {
tearDown();
});
return {
/**
* Leave the channel
*/
leaveChannel: tearDown,
/**
* Leave the channel and also its associated private and presence channels
*/
leave: () => tearDown(true),
/**
* Channel instance
*/
channel: () => subscription as ChannelReturnType<TDriver, TVisibility>,
};
}
export const usePresenceChannel = <
TDriver extends BroadcastDriver = BroadcastDriver,
>(
channelName: string,
) => {
return useChannel<TDriver, "presence">(channelName, "presence");
};
export const usePublicChannel = <
TDriver extends BroadcastDriver = BroadcastDriver,
>(
channelName: string,
) => {
return useChannel<TDriver, "public">(channelName, "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",
);
};
const useConnectionChange = (
callback: (status: ConnectionStatus) => void,
invokeOnMount: boolean = true,
): void => {
const echoInstance = echo();
let unsubscribe: (() => void) | undefined;
onMounted(() => {
if (invokeOnMount) {
callback(echoInstance.connectionStatus());
}
unsubscribe = echoInstance.connector.onConnectionChange(callback);
});
onUnmounted(() => {
unsubscribe?.();
});
};
export const useConnectionStatus = (): Ref<ConnectionStatus> => {
const echoInstance = echo();
const status = ref<ConnectionStatus>(echoInstance.connectionStatus());
useConnectionChange((newStatus) => {
status.value = newStatus;
});
return status;
};
export const useSocketId = (): Ref<string | undefined> => {
const echoInstance = echo();
const socketId = ref<string | undefined>(echoInstance.socketId());
useConnectionChange(() => {
socketId.value = echoInstance.socketId();
}, false);
return socketId;
};