My aggregated monorepo of OCaml code, automaintained
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Add odoc-spa-loaded event for extension SPA support

The SPA shell now dispatches a 'odoc-spa-loaded' custom event on
document after all external scripts from the fetched page have
loaded and inline scripts have executed. Extensions listen for
this event instead of polling for CDN library availability.

This replaces the unreliable polling approach — the MutationObserver
fires before CDN scripts load, and no further mutations occur to
trigger re-rendering. The custom event fires at exactly the right
moment: after content is swapped AND all scripts are ready.

Updated mermaid, dot, and msc extensions to use the new event.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

+24 -67
+9 -30
odoc-dot-extension/src/dot_extension.ml
··· 145 145 }); 146 146 } 147 147 148 - function init() { 149 - renderAll(); 150 - observe(); 151 - if (typeof Viz === 'undefined' && 152 - document.querySelectorAll('.odoc-dot-diagram:not([data-dot-init])').length > 0) { 153 - waitForViz(); 154 - } 155 - } 156 - 157 148 if (document.readyState === 'loading') { 158 - document.addEventListener('DOMContentLoaded', init); 149 + document.addEventListener('DOMContentLoaded', function() { 150 + renderAll(); 151 + observe(); 152 + }); 159 153 } else { 160 - init(); 154 + renderAll(); 155 + observe(); 161 156 } 162 157 163 158 function observe() { 164 - new MutationObserver(function() { 165 - renderAll(); 166 - if (typeof Viz === 'undefined' && 167 - document.querySelectorAll('.odoc-dot-diagram:not([data-dot-init])').length > 0) { 168 - waitForViz(); 169 - } 170 - }).observe(document.body, { childList: true, subtree: true }); 171 - } 172 - 173 - var waitTimer = null; 174 - function waitForViz() { 175 - if (waitTimer) return; 176 - waitTimer = setInterval(function() { 177 - if (typeof Viz !== 'undefined') { 178 - clearInterval(waitTimer); 179 - waitTimer = null; 180 - renderAll(); 181 - } 182 - }, 200); 159 + new MutationObserver(function() { renderAll(); }) 160 + .observe(document.body, { childList: true, subtree: true }); 161 + document.addEventListener('odoc-spa-loaded', function() { renderAll(); }); 183 162 } 184 163 })(); 185 164 |}
+3 -1
odoc-jons-plugins/src/odoc_jons_plugins_js.ml
··· 257 257 } 258 258 }); 259 259 260 - // After external scripts load, execute inline scripts (deduplicated) 260 + // After external scripts load, execute inline scripts (deduplicated), 261 + // then fire a custom event so extensions know all resources are ready. 261 262 Promise.all(newScriptLoadPromises).then(function() { 262 263 inlineScripts.forEach(function(el) { 263 264 var id = el.getAttribute('data-spa-inline'); ··· 265 266 var s = el.cloneNode(true); 266 267 document.head.appendChild(s); 267 268 }); 269 + document.dispatchEvent(new CustomEvent('odoc-spa-loaded')); 268 270 }); 269 271 270 272 // Update state
+11 -36
odoc-mermaid-extension/src/mermaid_extension.ml
··· 248 248 try { mermaid.run({ nodes: Array.from(pending) }); } catch(e) { console.error('mermaid.run error:', e); } 249 249 } 250 250 251 - function init() { 252 - renderAll(); 253 - observe(); 254 - // If mermaid isn't loaded yet but there are pending elements, 255 - // start polling immediately (handles SPA where this script loads 256 - // after content swap but before CDN script finishes). 257 - if (typeof mermaid === 'undefined' && 258 - document.querySelectorAll('pre.mermaid:not([data-mermaid-init])').length > 0) { 259 - waitForMermaid(); 260 - } 261 - } 262 - 263 251 if (document.readyState === 'loading') { 264 - document.addEventListener('DOMContentLoaded', init); 252 + document.addEventListener('DOMContentLoaded', function() { 253 + renderAll(); 254 + observe(); 255 + }); 265 256 } else { 266 - init(); 257 + renderAll(); 258 + observe(); 267 259 } 268 260 269 261 function observe() { 270 - new MutationObserver(function() { 271 - renderAll(); 272 - // If mermaid isn't loaded yet but there are pending elements, 273 - // poll until it loads (handles SPA where mermaid CDN script 274 - // loads after content swap). 275 - if (typeof mermaid === 'undefined' && 276 - document.querySelectorAll('pre.mermaid:not([data-mermaid-init])').length > 0) { 277 - waitForMermaid(); 278 - } 279 - }).observe(document.body, { childList: true, subtree: true }); 280 - } 281 - 282 - var waitTimer = null; 283 - function waitForMermaid() { 284 - if (waitTimer) return; 285 - waitTimer = setInterval(function() { 286 - if (typeof mermaid !== 'undefined') { 287 - clearInterval(waitTimer); 288 - waitTimer = null; 289 - renderAll(); 290 - } 291 - }, 200); 262 + // Re-render on DOM mutations (new content added dynamically) 263 + new MutationObserver(function() { renderAll(); }) 264 + .observe(document.body, { childList: true, subtree: true }); 265 + // Re-render after SPA navigation completes (all scripts loaded) 266 + document.addEventListener('odoc-spa-loaded', function() { renderAll(); }); 292 267 } 293 268 })(); 294 269 |}
+1
odoc-msc-extension/src/msc_extension.ml
··· 67 67 function observe() { 68 68 new MutationObserver(function() { renderAll(); }) 69 69 .observe(document.body, { childList: true, subtree: true }); 70 + document.addEventListener('odoc-spa-loaded', function() { renderAll(); }); 70 71 } 71 72 })(); 72 73 |} mscgen_js_url