--- /dev/null
+import { ref, onUnmounted } from "vue";
+
+// The socket
+const socket = ref<WebSocket | null>(null);
+
+// To check whether we are connected
+const isConnected = ref(false);
+
+const reconnectHoldoffTime = 3000; // 3 seconds
+let reconnectAttempts: number = 0;
+let reconnectTimeout: number | null = null;
+
+// Listeners
+const listeners: Map<string, Set<(data: any) => void>> = new Map();
+
+function connect() {
+ // Clear any reconnect timers
+ if (reconnectTimeout) {
+ clearTimeout(reconnectTimeout);
+ reconnectTimeout = null;
+ }
+
+ // Don't connect again once connected
+ if (socket.value && socket.value.readyState <= 1)
+ return;
+
+ // Connect the socket
+ socket.value = new WebSocket("/api/v1/events");
+
+ // Called when the socket connected
+ socket.value.onopen = () => {
+ // We are now connected
+ isConnected.value = true;
+ }
+
+ // Called when the socket was closed
+ socket.value.onclose = () => {
+ // We are now disconnected
+ isConnected.value = false;
+
+ // Reconnect soon
+ reconnect();
+ }
+
+ // Called when there has been an error
+ socket.value.onerror = () => {
+ socket.value?.close();
+ }
+
+ // Called when we received a message
+ socket.value.onmessage = (event) => {
+ try {
+ // Parse the event
+ const { type, payload } = JSON.parse(event.data);
+
+ // Call all callbacks
+ const callbacks = listeners.get(type);
+ if (callbacks) {
+ callbacks.forEach(cb => cb(payload));
+ }
+ } catch (e) {
+ console.error("Failed to parse event:", e);
+ }
+ };
+}
+
+// Closes the socket
+function close() {
+ socket.value?.close();
+}
+
+// Will schedule a reconnect
+function reconnect() {
+ if (reconnectTimeout)
+ return;
+
+ // Increment attempts
+ reconnectAttempts++;
+
+ // Reconnect again after the holdoff time
+ reconnectTimeout = setTimeout(() => connect(), reconnectHoldoffTime);
+}
+
+export function useEvents() {
+ // Connect to the API
+ connect();
+
+ // Subscribe a new event listener
+ function subscribe(type: string, cb: (data: any) => void) {
+ if (!listeners.has(type)) {
+ listeners.set(type, new Set());
+ }
+ listeners.get(type)!.add(cb);
+ }
+
+ // Remove the event listener
+ function unsubscribe(type: string, cb: (data: any) => void) {
+ listeners.get(type)?.delete(cb);
+ }
+
+ // Remove all listeners on cleanup
+ onUnmounted(() => {
+ listeners.clear();
+ });
+
+ return {
+ isConnected,
+ subscribe,
+ unsubscribe,
+ };
+}