Retire legacy OCR/media/evidence fallbacks, add local-folder event bus and Page Aggregate guards, and archive completed design checklists. Validation: cargo test -p mnote-web -- --test-threads=1; cargo test --workspace -- --test-threads=1; git diff --check; codegraph sync .; codegraph_status.
261 lines
10 KiB
JavaScript
261 lines
10 KiB
JavaScript
function readBootstrap() {
|
|
var script = document.getElementById('__MNOTE_TREE_LIVE_BOOTSTRAP__');
|
|
var fallback = {
|
|
schema: 'mnote.tree_live_bootstrap.v1',
|
|
transport: 'tree-live-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 || 'tree-live-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('tree-live-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()
|
|
|| (document.body instanceof HTMLElement ? (document.body.getAttribute('data-mnote-source-kind') || '').trim() : '');
|
|
if (sourceKind === 'local_folder' || bootstrap.transport === 'local-folder-events') {
|
|
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 localBus = window.__mnoteLocalFolderEventBus;
|
|
if (localBus && typeof localBus.startLocalFolderWatcher === 'function') {
|
|
var localHandle = localBus.startLocalFolderWatcher({
|
|
rootUri: localRootUri,
|
|
workspaceId: bootstrap.workspaceId || resolveWorkspaceId(),
|
|
bootstrap: bootstrap
|
|
});
|
|
if (localHandle) {
|
|
window.__mnoteTreeLiveEventSource = localHandle;
|
|
applyStatus('connected');
|
|
return;
|
|
}
|
|
}
|
|
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 — stay static and let caller surface events-required
|
|
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;
|
|
if (preferWs) {
|
|
applyTransport('tree-live-ws');
|
|
startWithWebSocket(bootstrap, workspaceId, bootstrap.wsEndpoint || '/api/realtime/ws');
|
|
return;
|
|
}
|
|
|
|
// Fallback: SSE / EventSource
|
|
if (!('EventSource' in window)) {
|
|
applyStatus('unsupported');
|
|
return;
|
|
}
|
|
applyTransport(requestedTransport || 'tree-live-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();
|
|
}
|
|
}
|