Summary
This issue documents the discovery and fix of a race condition bug when loading Mashlib (SolidOS data browser) from CDN, and introduces dual-mode support (local + CDN).
Problem Description
When serving RDF resources with Accept: text/html, JSS returns an HTML wrapper that loads Mashlib to render the data. Two critical bugs were discovered:
Bug 1: Recursive Nesting (CDN mode)
Symptoms:
- On first page load, the Mashlib UI renders recursively nested inside itself infinitely
- Hard refresh (Ctrl+Shift+R) fixes the issue temporarily
- Consistent reproduction in incognito/fresh browser sessions
Root Cause:
The original implementation used defer + DOMContentLoaded:
<script>document.addEventListener('DOMContentLoaded', function() {
panes.runDataBrowser()
})</script>
<script defer="defer" src="https://unpkg.com/mashlib@2.0.0/dist/mashlib.min.js"></script>
The defer attribute should make scripts execute after parsing but before DOMContentLoaded. However, with slow CDN loading, there's a race condition where:
- HTML parsing completes
DOMContentLoaded fires before the CDN script finishes downloading
panes is undefined or partially initialized
- Mashlib enters an error state causing recursive rendering
Bug 2: First-Load Blank Content (after partial fix)
Symptoms:
- After adding guards like
if (typeof panes !== 'undefined'), first load showed blank content
- Data was fetched (304 in network tab) but UI didn't render
- Hard refresh worked
Root Cause:
Mashlib internally creates document contexts/clones. Our guard scripts ran in these cloned contexts where panes wasn't defined, preventing initialization even when it should run.
Solution
1. CDN Mode: Use script.onload Pattern
Instead of relying on defer + DOMContentLoaded, dynamically create the script element and use onload:
if (cdnVersion) {
const cdnBase = `https://unpkg.com/mashlib@${cdnVersion}/dist`;
return `<!doctype html><html><head><meta charset="utf-8"/><title>SolidOS Web App</title>
<link href="${cdnBase}/mash.css" rel="stylesheet"></head>
<body id="PageBody"><header id="PageHeader"></header>
<div class="TabulatorOutline" id="DummyUUID" role="main"><table id="outline"></table><div id="GlobalDashboard"></div></div>
<footer id="PageFooter"></footer>
<script>
(function() {
var s = document.createElement('script');
s.src = '${cdnBase}/mashlib.min.js';
s.onload = function() { panes.runDataBrowser(); };
s.onerror = function() { document.body.innerHTML = '<p>Failed to load Mashlib from CDN</p>'; };
document.head.appendChild(s);
})();
</script></body></html>`;
}
This guarantees panes.runDataBrowser() only executes after mashlib.min.js is fully loaded and executed.
2. Local Mode: Match Official databrowser.html
For local hosting, use the exact HTML from mashlib's official databrowser.html:
return `<!doctype html><html><head><meta charset="utf-8"/><title>SolidOS Web App</title><script>document.addEventListener('DOMContentLoaded', function() {
panes.runDataBrowser()
})</script><script defer="defer" src="https://github.com/mashlib.min.js"></script><link href="https://github.com/mash.css" rel="stylesheet"></head><body id="PageBody"><header id="PageHeader"></header><div class="TabulatorOutline" id="DummyUUID" role="main"><table id="outline"></table><div id="GlobalDashboard"></div></div><footer id="PageFooter"></footer></body></html>`;
Local hosting eliminates the CDN latency that causes the race condition.
3. Cache Control Headers
Added Cache-Control: no-store to the HTML wrapper response to prevent browsers from caching the HTML and confusing it with the actual RDF content:
headers['Vary'] = 'Accept';
headers['Cache-Control'] = 'no-store';
CLI Options
Two modes are now supported:
| Flag |
Description |
Requires |
--mashlib |
Local mode - serve from src/mashlib-local/dist/ |
Local mashlib build |
--mashlib-cdn |
CDN mode - load from unpkg.com |
Nothing (zero footprint) |
--mashlib-version <ver> |
Specify CDN version (default: 2.0.0) |
--mashlib-cdn |
Files Changed
src/mashlib/index.js - generateDatabrowserHtml() now supports CDN mode with script.onload
src/server.js - Added mashlibCdn option and request decoration
src/handlers/resource.js - Pass CDN version to HTML generator, added cache headers
src/config.js - Added mashlibCdn default and env var mapping
bin/jss.js - Added --mashlib-cdn CLI option
Testing
CDN Mode Test
# Start server
jss start --port 4000 --mashlib-cdn --mashlib-version 2.0.0
# Verify HTML output
curl -H "Accept: text/html" http://localhost:4000/path/to/resource.ttl
# Should show script.onload pattern with unpkg.com URLs
Local Mode Test
# Start server (requires mashlib built locally)
jss start --port 4000 --mashlib
# Verify HTML output
curl -H "Accept: text/html" http://localhost:4000/path/to/resource.ttl
# Should show defer pattern with /mashlib.min.js
Browser Test
- Open incognito window
- Navigate to an RDF resource URL
- First load should render correctly (no recursive nesting, no blank content)
- Console should only show expected "Unable to fetch your preferences" warning (normal for anonymous users)
Why This Matters
- CDN mode keeps JSS lightweight (no bundled mashlib ~2MB+)
- Local mode provides reliability for production deployments
- Both modes now work correctly on first load without race conditions
Related
This issue was created with assistance from Claude Code during a debugging session that identified and fixed the race condition.
Summary
This issue documents the discovery and fix of a race condition bug when loading Mashlib (SolidOS data browser) from CDN, and introduces dual-mode support (local + CDN).
Problem Description
When serving RDF resources with
Accept: text/html, JSS returns an HTML wrapper that loads Mashlib to render the data. Two critical bugs were discovered:Bug 1: Recursive Nesting (CDN mode)
Symptoms:
Root Cause:
The original implementation used
defer+DOMContentLoaded:The
deferattribute should make scripts execute after parsing but beforeDOMContentLoaded. However, with slow CDN loading, there's a race condition where:DOMContentLoadedfires before the CDN script finishes downloadingpanesis undefined or partially initializedBug 2: First-Load Blank Content (after partial fix)
Symptoms:
if (typeof panes !== 'undefined'), first load showed blank contentRoot Cause:
Mashlib internally creates document contexts/clones. Our guard scripts ran in these cloned contexts where
paneswasn't defined, preventing initialization even when it should run.Solution
1. CDN Mode: Use
script.onloadPatternInstead of relying on
defer+DOMContentLoaded, dynamically create the script element and useonload:This guarantees
panes.runDataBrowser()only executes after mashlib.min.js is fully loaded and executed.2. Local Mode: Match Official databrowser.html
For local hosting, use the exact HTML from mashlib's official
databrowser.html:Local hosting eliminates the CDN latency that causes the race condition.
3. Cache Control Headers
Added
Cache-Control: no-storeto the HTML wrapper response to prevent browsers from caching the HTML and confusing it with the actual RDF content:CLI Options
Two modes are now supported:
--mashlibsrc/mashlib-local/dist/--mashlib-cdn--mashlib-version <ver>--mashlib-cdnFiles Changed
src/mashlib/index.js-generateDatabrowserHtml()now supports CDN mode withscript.onloadsrc/server.js- AddedmashlibCdnoption and request decorationsrc/handlers/resource.js- Pass CDN version to HTML generator, added cache headerssrc/config.js- AddedmashlibCdndefault and env var mappingbin/jss.js- Added--mashlib-cdnCLI optionTesting
CDN Mode Test
Local Mode Test
Browser Test
Why This Matters
Related
This issue was created with assistance from Claude Code during a debugging session that identified and fixed the race condition.