// composables/usePoll.ts import { ref, watch, onUnmounted } from 'vue'; import * as Y from 'yjs'; export const usePoll = (pollId: Ref, user: Ref) => { const pollData = ref({}); const isConnected = ref(false); const connectionAttempFailed = ref(false); const connectedPeers = ref(1); let ydoc: Y.Doc | null = null; let provider: any = null; let yMap: Y.Map[]> | null = null; const cleanup = () => { if (provider) provider.disconnect(); if (ydoc) ydoc.destroy(); isConnected.value = false; pollData.value = {}; }; const initPoll = async (id: string) => { cleanup(); // Clear previous session ydoc = new Y.Doc(); // 1. Fetch Snapshot from Nuxt API try { const response = await $fetch<{ update: number[] | null }>(`/api/polls/${id}`).catch((e) => { console.error("Failed to get poll: " + id,e) }); //trust the server without verification. if (response?.update) { Y.applyUpdate(ydoc, new Uint8Array(response.update)); } } catch (err) { console.error('Persistence fetch failed', err); } yMap = ydoc.getMap[]>('shared-poll'); // 2. Local State Sync yMap.observe(async () => { await performUpdateAndVerify(); saveStateToServer(id); }); await performUpdateAndVerify(); // 3. P2P Connection const { WebrtcProvider } = await import('y-webrtc'); provider = new WebrtcProvider(`nuxt-p2p-${id}`, ydoc, { signaling: ["ws://localhost:4444", "ws://lynxpi.ddns.net:4444"] }); provider.on('synced', (arg: {synced: boolean}) => { isConnected.value = arg.synced; console.log('Connection synced:', arg.synced) // "connected" or "disconnected" }); provider.on('status', (event: { connected: boolean }) => { console.log('Connection status:', event.connected) // "connected" or "disconnected" }) provider.on('peers', (data: any) => { connectedPeers.value = data.webrtcPeers.length + 1 }); }; const saveStateToServer = async (id: string) => { if (!ydoc) return; const stateUpdate = Y.encodeStateAsUpdate(ydoc); await $fetch(`/api/polls/${id}`, { method: 'POST', body: { update: Array.from(stateUpdate) } }).catch((e) => { console.error("Failed to update poll",e) }); }; // Watch for ID changes (e.g., user clicks a link or goes back) watch(pollId, (newId) => { if (newId && import.meta.client) { initPoll(newId); } else { cleanup(); } }, { immediate: true }); onUnmounted(cleanup); const addOption = (optionName: string) => { if (yMap && !yMap.has(optionName)) yMap.set(optionName, []); }; const performUpdateAndVerify = async () => { const pollDataUpdate = yMap!.toJSON(); console.log("Poll Data Update: ", pollDataUpdate) for(var option in pollDataUpdate){ console.log("verifying votes for option: " + option); const votes = pollDataUpdate[option] || []; const verified = await verifyAllVotesForOption(votes); if(!verified){ console.error("Failed to verify option: "+option) return; } } console.log("All options verified! :)") pollData.value = pollDataUpdate } const vote = async (optionName: string) => { const currentUser = user.value; if (currentUser != undefined && yMap?.has(optionName)) { const voteData = [...(yMap.get(optionName) || [])]; if(voteData != undefined && currentUser.private_key){ var unsignedVoteData : VoteData = { userid: currentUser.userid, timestamp: new Date().toISOString() } var newVote : SignedData = { data: unsignedVoteData, signature: "", } voteData?.push(newVote) const signature = await signVote(voteData,currentUser.private_key); newVote.signature=signature yMap?.set(optionName, voteData); } } }; return { pollData, isConnected, connectionAttempFailed, connectedPeers, addOption, vote }; };