Files
mnote/rust/crates/mnote-web/browser/tree-live-controller.js
T

247 lines
9.6 KiB
JavaScript
Raw Normal View History

2026-05-24 21:35:21 +08:00
function readBootstrap() {
var script = document.getElementById('__MNOTE_TREE_LIVE_BOOTSTRAP__');
var fallback = {
schema: 'mnote.tree_live_bootstrap.v1',
transport: 'tree-live-sse',
2026-05-24 21:35:21 +08:00
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 || 'tree-live-sse');
2026-05-24 21:35:21 +08:00
}
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 });
});
2026-05-24 21:35:21 +08:00
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('tree-live-sse');
2026-05-24 21:35:21 +08:00
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 requestedTransport = bootstrap.transport || 'tree-live-sse';
if (requestedTransport === 'convex-command-log-sse') requestedTransport = 'tree-live-sse';
if (requestedTransport === 'convex-command-log-ws') requestedTransport = 'tree-live-ws';
var preferWs = requestedTransport === 'tree-live-ws' && 'WebSocket' in window;
2026-05-24 21:35:21 +08:00
if (preferWs) {
applyTransport('tree-live-ws');
2026-05-24 21:35:21 +08:00
startWithWebSocket(bootstrap, workspaceId, bootstrap.wsEndpoint || '/api/realtime/ws');
return;
}
// Fallback: SSE / EventSource
if (!('EventSource' in window)) {
applyStatus('unsupported');
return;
}
applyTransport(requestedTransport || 'tree-live-sse');
2026-05-24 21:35:21 +08:00
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();
}
}