function readBootstrap() { var script = document.getElementById('__MNOTE_TREE_LIVE_BOOTSTRAP__'); var fallback = { schema: 'mnote.tree_live_bootstrap.v1', transport: 'convex-command-log-sse', endpoint: '/api/tree/events', rootIds: [], initialRevision: null }; if (!script || !script.textContent) return fallback; try { return Object.assign(fallback, JSON.parse(script.textContent)); } catch (_) { return fallback; } } function resolveWorkspaceId() { var withWorkspace = document.querySelector('[data-workspace-id]'); if (withWorkspace) { var value = (withWorkspace.getAttribute('data-workspace-id') || '').trim(); if (value) return value; } var params = new URLSearchParams(window.location.search); return (params.get('workspaceId') || 'default').trim() || 'default'; } function dispatchTreeEvent(name, detail) { window.dispatchEvent(new CustomEvent(name, { detail: detail })); } function applyStatus(status) { document.documentElement.setAttribute('data-mnote-tree-live-status', status); } function applyTransport(transport) { document.documentElement.setAttribute('data-mnote-tree-live-transport', transport || 'convex-command-log-sse'); } function closeActiveSource() { var source = window.__mnoteTreeLiveEventSource; if (source && typeof source.close === 'function') { source.close(); window.__mnoteTreeLiveEventSource = null; applyStatus('closed'); } } function liveDeltaNeedsResync(payload) { var data = payload && (payload.data || payload.delta || payload); var op = data && typeof data.op === 'string' ? String(data.op).trim() : ''; if (!op || op === 'noop') return false; if (op === 'resync_required') return true; if (data && data.requiresResync === true) return true; return false; } function startWithSse(bootstrap, workspaceId, url) { var failures = 0; var source = new EventSource(url.toString()); window.__mnoteTreeLiveEventSource = source; applyStatus('connecting'); source.addEventListener('open', function(){ failures = 0; applyStatus('connected'); }); source.addEventListener('snapshot', function(event){ var payload = JSON.parse(event.data || '{}'); var revision = payload.revision || payload.cursor || event.lastEventId || null; document.documentElement.setAttribute('data-mnote-tree-live-revision', String(revision || '')); dispatchTreeEvent('tree:snapshot', { payload: payload, revision: revision, bootstrap: bootstrap }); }); source.addEventListener('delta', function(event){ var payload = JSON.parse(event.data || '{}'); var revision = payload.revision || payload.cursor || event.lastEventId || null; document.documentElement.setAttribute('data-mnote-tree-live-revision', String(revision || '')); dispatchTreeEvent('tree:delta', { payload: payload, revision: revision, bootstrap: bootstrap }); }); source.addEventListener('resync', function(event){ var payload = JSON.parse(event.data || '{}'); var revision = payload.revision || payload.cursor || event.lastEventId || null; document.documentElement.setAttribute('data-mnote-tree-live-revision', String(revision || '')); dispatchTreeEvent('tree:resync', { payload: payload, revision: revision, bootstrap: bootstrap }); }); source.addEventListener('watch_batch', function(event){ var payload = JSON.parse(event.data || '{}'); var revision = payload.revision || payload.cursor || event.lastEventId || null; document.documentElement.setAttribute('data-mnote-tree-live-revision', String(revision || '')); dispatchTreeEvent('tree:local-folder-watch-batch', { payload: payload, revision: revision, bootstrap: bootstrap }); }); source.addEventListener('tree_error', function(event){ var payload = JSON.parse(event.data || '{}'); dispatchTreeEvent('tree:error', { payload: payload, bootstrap: bootstrap }); }); source.addEventListener('block.delta', function(event){ var payload = JSON.parse(event.data || '{}'); dispatchTreeEvent('tree:block-delta', { payload: payload, bootstrap: bootstrap }); }); source.onerror = function(){ failures += 1; applyStatus(failures >= 3 ? 'resync-pending' : 'reconnecting'); }; } function startWithWebSocket(bootstrap, workspaceId, wsUrl) { var proto = (window.location.protocol === 'https:') ? 'wss:' : 'ws:'; var url = new URL(wsUrl || bootstrap.wsEndpoint || '/api/realtime/ws', window.location.origin); url.protocol = proto; url.searchParams.set('workspaceId', workspaceId); if (Array.isArray(bootstrap.rootIds) && bootstrap.rootIds.length === 1) { url.searchParams.set('rootNodeId', bootstrap.rootIds[0]); } var ws = new WebSocket(url.toString()); window.__mnoteTreeLiveEventSource = ws; applyStatus('connecting'); ws.onopen = function() { applyStatus('connected'); }; ws.onmessage = function(event) { var payload; try { payload = JSON.parse(event.data); } catch (_) { return; } var kind = payload.kind || ''; var revision = payload.revision || payload.cursor || ''; if (kind === 'snapshot') { document.documentElement.setAttribute('data-mnote-tree-live-revision', String(revision)); dispatchTreeEvent('tree:snapshot', { payload: payload, revision: revision, bootstrap: bootstrap }); } else if (kind === 'delta') { document.documentElement.setAttribute('data-mnote-tree-live-revision', String(revision)); dispatchTreeEvent('tree:delta', { payload: payload, revision: revision, bootstrap: bootstrap }); if (liveDeltaNeedsResync(payload) && ws.readyState === WebSocket.OPEN) { ws.send('resync'); } } else if (kind === 'resync') { document.documentElement.setAttribute('data-mnote-tree-live-revision', String(revision)); dispatchTreeEvent('tree:resync', { payload: payload, revision: revision, bootstrap: bootstrap }); } else if (kind === 'resync_hint') { // Server suggests re-sync after lagged events if (ws.readyState === WebSocket.OPEN) { ws.send('resync'); } } }; ws.onclose = function() { applyStatus('closed'); // Fall back to SSE after a short delay setTimeout(function() { if (window.__mnoteTreeLiveEventSource === ws) { startWithSseFallback(bootstrap, workspaceId); } }, 2000); }; ws.onerror = function() { applyStatus('error'); }; } function startWithSseFallback(bootstrap, workspaceId) { applyTransport('convex-command-log-sse'); var url = new URL(bootstrap.endpoint || '/api/tree/events', window.location.origin); url.searchParams.set('workspaceId', workspaceId); if (Array.isArray(bootstrap.rootIds) && bootstrap.rootIds.length === 1) { url.searchParams.set('rootNodeId', bootstrap.rootIds[0]); } startWithSse(bootstrap, workspaceId, url); } function start() { var bootstrap = readBootstrap(); if (bootstrap.disabled === true || bootstrap.transport === 'disabled') { applyTransport('disabled'); applyStatus('disabled'); return; } var params = new URLSearchParams(window.location.search); var sourceKind = (params.get('sourceKind') || '').trim(); if (sourceKind === 'local_folder') { applyTransport('local-folder-events'); applyStatus('connecting'); var localRootUri = (params.get('rootUri') || '').trim() || (document.body instanceof HTMLElement ? (document.body.getAttribute('data-mnote-root-uri') || '').trim() : ''); if (localRootUri && 'EventSource' in window) { var url = new URL('/api/local-folder/events', window.location.origin); url.searchParams.set('rootUri', localRootUri); url.searchParams.set('treeLive', 'true'); startWithSse(bootstrap, '', url); return; } // No rootUri or EventSource unavailable — mark static and let polling fallback handle it applyTransport('local-folder-static'); applyStatus('static'); return; } var workspaceId = bootstrap.workspaceId || resolveWorkspaceId(); // Prefer WebSocket transport when available var preferWs = bootstrap.transport === 'convex-command-log-ws' && 'WebSocket' in window; if (preferWs) { applyTransport('convex-command-log-ws'); startWithWebSocket(bootstrap, workspaceId, bootstrap.wsEndpoint || '/api/realtime/ws'); return; } // Fallback: SSE / EventSource if (!('EventSource' in window)) { applyStatus('unsupported'); return; } applyTransport(bootstrap.transport || 'convex-command-log-sse'); var url = new URL(bootstrap.endpoint || '/api/tree/events', window.location.origin); url.searchParams.set('workspaceId', workspaceId); if (Array.isArray(bootstrap.rootIds) && bootstrap.rootIds.length === 1) { url.searchParams.set('rootNodeId', bootstrap.rootIds[0]); } startWithSse(bootstrap, workspaceId, url); } if (!window.__mnoteTreeLiveControllerStarted) { window.__mnoteTreeLiveControllerStarted = true; window.__mnoteTreeLiveClose = closeActiveSource; window.addEventListener('pagehide', closeActiveSource, { once: true }); if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', start, { once: true }); } else { start(); } }