note
connecting
app.js import * as Y from 'yjs' import { WebsocketProvider } from 'y-websocket' import { marked } from 'marked' const wsProtocol = location.protocol === "https:" ? "wss://" : "ws://"; const wsUrl = wsProtocol + location.host; const room = location.pathname.slice(1) || "default"; const ydoc = new Y.Doc(); const ytext = ydoc.getText("shared"); const provider = new WebsocketProvider(wsUrl, room, ydoc, { connect: true, maxBackoffTime: 5000 }); const awareness = provider.awareness; const editor = document.getElementById("editor"); const preview = document.getElementById("preview"); const usersEl = document.getElementById("users"); const nameInput = document.getElementById("name"); const statusEl = document.getElementById("status"); marked.setOptions({ breaks: true }); /* ---------- Connection State ---------- */ provider.on('status', event => { if (event.status === 'connected') { statusEl.textContent = 'connected'; statusEl.className = 'badge success'; } else if (event.status === 'connecting') { statusEl.textContent = 'connecting'; statusEl.className = 'badge warning'; } else { statusEl.textContent = 'disconnected'; statusEl.className = 'badge danger'; } }); /* ---------- Identity ---------- */ const name = localStorage.getItem("name") || "user-" + Math.floor(Math.random()*1000); nameInput.value = name; function setUser(name) { localStorage.setItem("name", name); awareness.setLocalStateField("user", { name }); } setUser(name); nameInput.addEventListener("input", () => setUser(nameInput.value)); /* ---------- Awareness UI ---------- */ awareness.on('change', () => { const states = Array.from(awareness.getStates().values()); usersEl.innerHTML = ''; states.forEach(s => { if (!s.user?.name) return; const el = document.createElement('div'); el.className = 'badge'; el.textContent = s.user.name; usersEl.appendChild(el); }); }); /* ---------- Markdown ---------- */ let mdTimeout; function renderMarkdown(text) { clearTimeout(mdTimeout); mdTimeout = setTimeout(() => { preview.innerHTML = marked.parse(text); }, 60); } /* ---------- Cursor Handling ---------- */ function getCursorPos() { const sel = window.getSelection(); return sel?.anchorOffset || 0; } function setCursorPos(pos) { const node = editor.firstChild; if (!node) return; const range = document.createRange(); const sel = window.getSelection(); range.setStart(node, Math.min(pos, node.length)); range.collapse(true); sel.removeAllRanges(); sel.addRange(range); } /* ---------- CRDT → DOM ---------- */ ytext.observe(event => { if (event.transaction.origin === "local") return; const val = ytext.toString(); if (editor.innerText !== val) { const pos = getCursorPos(); editor.innerText = val; setCursorPos(pos); } renderMarkdown(val); }); /* ---------- DOM → CRDT ---------- */ let inputTimeout; editor.addEventListener("input", () => { clearTimeout(inputTimeout); inputTimeout = setTimeout(() => { const text = editor.innerText; ydoc.transact(() => { ytext.delete(0, ytext.length); ytext.insert(0, text); }, "local"); }, 40); }); /* ---------- Graceful Cleanup ---------- */ window.addEventListener("beforeunload", () => { provider.destroy(); ydoc.destroy(); }); renderMarkdown("");