Skip to content

Round-trip optimization for HTML resource views (#346) - #347

Merged
melvincarvalho merged 7 commits into
gh-pagesfrom
issue-346-round-trip-optimization
May 2, 2026
Merged

Round-trip optimization for HTML resource views (#346)#347
melvincarvalho merged 7 commits into
gh-pagesfrom
issue-346-round-trip-optimization

Conversation

@melvincarvalho

Copy link
Copy Markdown
Contributor

Summary

Implements the round-trip optimization described in #346. JSS-served HTML wrappers now include a small inline reader script that lets RDF clients resolve fetcher.load(uri) from the inline JSON-LD data island instead of issuing a second HTTP request. Halves network round-trips for browser views.

What changed

  • src/mashlib/index.js:

    • New roundTripOptimizationScript() helper returning the inline <script> block
    • New opts.roundTripOptimization option on generateDatabrowserHtml() and generateModuleDatabrowserHtml() (default true; pass false to opt out)
    • Reader injected after the data island in body so the DOM element exists when the reader queries it
  • test/round-trip-optimization.test.js: 15 new tests covering presence in CDN/local/module modes, opt-out flag, accessor shape, retry-bound, fall-through, and absence of premature </script> close.

What the reader does

  1. Exposes window.__dataIsland.get(uri) — a generic accessor any client can use, returns { contentType, content } for a matching data-island element or null.
  2. If rdflib ($rdf.fetcher) loads, patches fetcher.load() to check the accessor first and parse inline data into the store; falls through to original network fetch on miss or parse error.
  3. Bounded retry: gives up after ~10 seconds (100 × 100ms) if rdflib never loads, so non-rdflib clients see no infinite polling.
  4. Self-guards against double-patching via __dataIslandPatched flag.

Verification

All 571 tests pass (npm test), including:

Side benefits

  • Offline support: saved HTML continues to render
  • Static export viability: HTML pages are self-contained
  • Reduced server load: ~half the requests for browser views
  • Better Core Web Vitals: faster initial render, less network dependency
  • Aligns with modern web-perf practices (inlining critical resources, eliminating waterfall round-trips)

Closes #346.

Inline a small reader script in the mashlib HTML wrapper that exposes
window.__dataIsland.get(uri) as a generic accessor and patches
$rdf.fetcher.load() (when rdflib is present) to resolve from the inline
JSON-LD data island instead of issuing a second HTTP request.

Bounded retry (~10s) prevents infinite polling on non-rdflib clients.
Falls through to original network fetch on any miss or parse error.
Default-on; opt-out via opts.roundTripOptimization=false.

Closes #346.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds an inline “data island reader” to JSS-generated Mashlib HTML wrappers so rdflib-based clients can satisfy fetcher.load(uri) directly from the embedded JSON-LD data island, avoiding a second HTTP fetch and reducing round-trips (per #346).

Changes:

  • Introduces roundTripOptimizationScript() and an opts.roundTripOptimization (default true) flag on Mashlib HTML generators.
  • Injects the reader script into CDN/local/module HTML wrappers immediately after the data island.
  • Adds a new unit test suite to assert reader emission/opt-out and basic invariants (retry bound, no </script> token, etc.).

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

File Description
src/mashlib/index.js Adds the inline reader script and a default-on opt-out flag, and injects it into generated HTML wrappers.
test/round-trip-optimization.test.js Adds tests validating the reader’s presence/absence and some safety/structure properties in emitted HTML.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread test/round-trip-optimization.test.js Outdated
it('exposes the documented generic accessor shape', () => {
const html = generateDatabrowserHtml('https://x.test/foo', '2.0.0');
// window.__dataIsland.get(uri) is the public surface
assert.match(html, /__dataIsland=window\.__dataIsland\|\|\{get:function/);
Comment thread test/round-trip-optimization.test.js Outdated
* `window.__dataIsland.get(uri)` and (when rdflib loads) patches
* `$rdf.fetcher.load()` to resolve from the inline JSON-LD data island
* instead of issuing a second HTTP request. These tests pin:
* - presence in CDN, local, and module HTML wrappers when default
Comment thread src/mashlib/index.js Outdated
Comment on lines +89 to +91
*/
function roundTripOptimizationScript() {
return `<script>(function(){if(typeof window==='undefined')return;window.__dataIsland=window.__dataIsland||{get:function(uri){if(!uri)return null;try{var esc=window.CSS&&CSS.escape?CSS.escape(uri):String(uri).replace(/["\\\\]/g,'\\\\$&');var el=document.querySelector('script#dataisland[data-uri="'+esc+'"]');if(el&&el.type==='application/ld+json')return{contentType:'application/ld+json',content:el.textContent};}catch(e){}return null;}};var n=0;(function p(){if(++n>100)return;if(typeof $rdf==='undefined'||!$rdf.fetcher||!$rdf.fetcher.load){setTimeout(p,100);return;}if($rdf.fetcher.__dataIslandPatched)return;$rdf.fetcher.__dataIslandPatched=true;var f=$rdf.fetcher,orig=f.load.bind(f);f.load=function(uri,options){var s=(uri&&uri.uri)||(uri&&uri.value)||String(uri);var d=window.__dataIsland.get(s);if(d){return new Promise(function(resolve,reject){$rdf.parse(d.content,f.store,s,d.contentType,function(err){if(err)reject(err);else{f.requested[s]='done';resolve($rdf.sym?$rdf.sym(s):s);}});}).catch(function(){return orig(uri,options);});}return orig(uri,options);};})();})();</script>`;
- Replace brittle accessor-shape test with public-surface assertion
  (no minified-token coupling)
- Fix doc-comment grammar in test header
- Add runtime-behavior suite (9 tests) using Node `vm` to evaluate
  the reader script with stubbed window/document/$rdf, pinning:
  - accessor returns {contentType, content} on island hit
  - accessor returns null on miss / falsy uri
  - synchronous patch when rdflib is already present
  - patched load resolves from data island, no network call
  - patched load falls through to original on miss
  - patched load falls through to original on parse error
  - patch is idempotent across repeat invocations
  - no-rdflib path exits silently, accessor still set up
- Reader: use `window.CSS.escape` instead of bare `CSS.escape`
  (consistent with the `window.CSS` guard; works in stricter contexts)

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread test/round-trip-optimization.test.js Outdated
Comment on lines +100 to +115
it('reader body does not contain a literal </script> token', () => {
// The reader is itself a <script> block; any literal end-tag inside
// its body would terminate the element prematurely.
const html = generateDatabrowserHtml('https://x.test/foo', '2.0.0');
// Extract just the reader script body and check it has no </script>
// We grep for the IIFE we know is in the reader and assert no end-tag
// inside the surrounding <script>...</script> the reader uses.
const readerStart = html.indexOf('window.__dataIsland');
assert.ok(readerStart > 0, 'reader script not found');
// Walk forward until we find the closing </script> for the reader
const readerEnd = html.indexOf('</script>', readerStart);
assert.ok(readerEnd > readerStart, 'reader script not properly closed');
const body = html.slice(readerStart, readerEnd);
assert.doesNotMatch(body, /<\/script>/i,
'reader body must not contain </script> token');
});
Comment thread test/round-trip-optimization.test.js Outdated
Comment on lines +182 to +185
function runReader(ctx, html) {
vm.runInContext(extractReaderSource(ctx.html || html), ctx);
}

Comment thread src/mashlib/index.js Outdated
* island, the page renders with one HTTP round-trip instead of two.
*/
function roundTripOptimizationScript() {
return `<script>(function(){if(typeof window==='undefined')return;window.__dataIsland=window.__dataIsland||{get:function(uri){if(!uri)return null;try{var esc=window.CSS&&window.CSS.escape?window.CSS.escape(uri):String(uri).replace(/["\\\\]/g,'\\\\$&');var el=document.querySelector('script#dataisland[data-uri="'+esc+'"]');if(el&&el.type==='application/ld+json')return{contentType:'application/ld+json',content:el.textContent};}catch(e){}return null;}};var n=0;(function p(){if(++n>100)return;if(typeof $rdf==='undefined'||!$rdf.fetcher||!$rdf.fetcher.load){setTimeout(p,100);return;}if($rdf.fetcher.__dataIslandPatched)return;$rdf.fetcher.__dataIslandPatched=true;var f=$rdf.fetcher,orig=f.load.bind(f);f.load=function(uri,options){var s=(uri&&uri.uri)||(uri&&uri.value)||String(uri);var d=window.__dataIsland.get(s);if(d){return new Promise(function(resolve,reject){$rdf.parse(d.content,f.store,s,d.contentType,function(err){if(err)reject(err);else{f.requested[s]='done';resolve($rdf.sym?$rdf.sym(s):s);}});}).catch(function(){return orig(uri,options);});}return orig(uri,options);};})();})();</script>`;
Comment thread src/mashlib/index.js Outdated
* island, the page renders with one HTTP round-trip instead of two.
*/
function roundTripOptimizationScript() {
return `<script>(function(){if(typeof window==='undefined')return;window.__dataIsland=window.__dataIsland||{get:function(uri){if(!uri)return null;try{var esc=window.CSS&&window.CSS.escape?window.CSS.escape(uri):String(uri).replace(/["\\\\]/g,'\\\\$&');var el=document.querySelector('script#dataisland[data-uri="'+esc+'"]');if(el&&el.type==='application/ld+json')return{contentType:'application/ld+json',content:el.textContent};}catch(e){}return null;}};var n=0;(function p(){if(++n>100)return;if(typeof $rdf==='undefined'||!$rdf.fetcher||!$rdf.fetcher.load){setTimeout(p,100);return;}if($rdf.fetcher.__dataIslandPatched)return;$rdf.fetcher.__dataIslandPatched=true;var f=$rdf.fetcher,orig=f.load.bind(f);f.load=function(uri,options){var s=(uri&&uri.uri)||(uri&&uri.value)||String(uri);var d=window.__dataIsland.get(s);if(d){return new Promise(function(resolve,reject){$rdf.parse(d.content,f.store,s,d.contentType,function(err){if(err)reject(err);else{f.requested[s]='done';resolve($rdf.sym?$rdf.sym(s):s);}});}).catch(function(){return orig(uri,options);});}return orig(uri,options);};})();})();</script>`;
- Reformat reader as multi-line readable JS (was single minified line);
  easier to maintain and security-review. Browser cost is negligible
  (~30 lines of inline JS, all in one IIFE).
- Close the race where mashlib loads and synchronously calls
  panes.runDataBrowser() (and hence fetcher.load) before the 100ms
  poll could fire: install Object.defineProperty getter/setter on
  window.$rdf so the patch applies the moment $rdf is assigned. Falls
  back to polling if the property is non-configurable.
- Strengthen the </script>-token test: assert exactly one </script>
  in the reader source (via newly-exported roundTripOptimizationScript)
  rather than slicing through HTML, where a premature close would have
  silently passed as the terminator.
- Remove dead runReader() helper.
- Add two new runtime tests: setter is installed on window.$rdf;
  setter patches fetcher immediately on assignment.
- Make remaining emission-suite regexes whitespace-tolerant since the
  reader is no longer minified.
- Export roundTripOptimizationScript so tests can assert on its source
  directly.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/mashlib/index.js
Comment on lines +129 to +151
function applyPatch(rdf) {
if (!rdf || !rdf.fetcher || !rdf.fetcher.load) return;
if (rdf.fetcher.__dataIslandPatched) return;
rdf.fetcher.__dataIslandPatched = true;
var f = rdf.fetcher;
var orig = f.load.bind(f);
f.load = function (uri, options) {
var s = (uri && uri.uri) || (uri && uri.value) || String(uri);
var d = window.__dataIsland.get(s);
if (d) {
return new Promise(function (resolve, reject) {
rdf.parse(d.content, f.store, s, d.contentType, function (err) {
if (err) {
reject(err);
} else {
f.requested[s] = 'done';
resolve(rdf.sym ? rdf.sym(s) : s);
}
});
}).catch(function () { return orig(uri, options); });
}
return orig(uri, options);
};
Comment thread test/round-trip-optimization.test.js Outdated
Comment on lines +178 to +180
return vm.createContext({
window, document, $rdf,
setTimeout, clearTimeout, Promise, String, console,
- Patched fetcher.load() now resolves to a Response-shaped object on
  data-island hits, matching the orig() path's return type. Consumers
  that inspect .ok / .status / .url / .headers.get(...) get consistent
  shape regardless of whether the island was hit or fall-through fired.
- Stub setTimeout/clearTimeout in vm test contexts so the reader's
  polling fallback (~10s of 100ms ticks when $rdf is absent) does not
  register real Node timers that keep the test process alive past the
  assertions. Suite duration: 10s → 89ms for the round-trip file;
  ~10s shaved off full suite.
- Extend the data-island-hit runtime test to assert the Response-
  shaped return contract (ok, status, url, headers.get).

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/mashlib/index.js Outdated
Comment on lines +112 to +118
var esc = window.CSS && window.CSS.escape
? window.CSS.escape(uri)
: String(uri).replace(/["\\\\]/g, '\\\\$&');
var el = document.querySelector(
'script#dataisland[data-uri="' + esc + '"]'
);
if (el && el.type === 'application/ld+json') {
Comment thread src/mashlib/index.js Outdated
Comment on lines +144 to +155
f.requested[s] = 'done';
// Return a Response-shaped object so consumers that
// inspect the resolved value (e.g. Response.ok, .status,
// .url, .headers.get) don't break compared with the
// original network path.
resolve({
ok: true,
status: 200,
statusText: 'OK',
url: s,
headers: { get: function () { return null; } }
});
- Reader fast path now resolves to a real `new Response(d.content,
  { status, statusText, headers })` when the constructor is available,
  with `url` overridden via Object.defineProperty so consumers reading
  it get the resource URL rather than the constructed-Response empty
  default. Falls back to a Response-shaped plain object in environments
  where the Response constructor is missing. Fixes the shape parity
  concern with consumers that use `instanceof Response` or `.text()`.
- Replace CSS-selector construction in `__dataIsland.get()` with
  `document.getElementById('dataisland')` + `getAttribute('data-uri')`
  string compare. Avoids CSS.escape pitfalls and any selector-injection
  surface in older browsers without `CSS.escape`. Single-island-per-page
  contract is preserved.
- Guard the success-path callback inside `rdf.parse(...)` with
  try/catch and check `f.requested` is an object before assigning, so
  unexpected throws (e.g. missing/non-writable `requested` on some
  rdflib builds) reject the surrounding Promise instead of leaving it
  hanging.
- Update the vm test context: stub `getElementById` instead of
  `querySelector`; expose Response in the context. Add three new
  runtime tests pinning: real Response return when available,
  Response-shaped fallback when not, and graceful resolution when
  fetcher.requested is missing (with a 500ms hung-promise guard).
- Fix unintended template-literal early-termination caused by
  inline-code backticks in a comment within the reader source.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/mashlib/index.js Outdated
status: 200,
statusText: 'OK',
url: s,
headers: { get: function () { return null; } }
Comment thread src/mashlib/index.js Outdated
Comment on lines +108 to +112
window.__dataIsland = window.__dataIsland || {
get: function (uri) {
if (!uri) return null;
try {
// Fetch by id and compare data-uri as a string. Avoids
- Initialize window.__dataIsland defensively: preserve any pre-existing
  truthy object but always ensure `.get` is a callable function. Avoids
  TypeErrors when another script set a partial __dataIsland without
  `.get`. Existing custom `.get` implementations are not overwritten.
- Fallback Response-shaped headers.get() now returns d.contentType for
  'content-type' (case-insensitive), null for unknown headers, and null
  for non-string names. Matches real Response.headers.get() semantics
  on the inline-data path.
- Add three runtime tests pinning: case-insensitive content-type lookup
  in fallback headers; preserved-but-augmented __dataIsland when prior
  object lacks .get; non-overwrite of pre-existing custom .get.
- Fix recurring backtick-in-comment template-literal early-termination
  by removing the inline-code backticks from the new defensive-init
  comment.

The repeated return-shape comment from Copilot has been addressed in
rounds 3 and 4 (real `new Response()` when available, Response-shaped
fallback otherwise, with url overridden via Object.defineProperty).

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/mashlib/index.js Outdated
// window.__dataIsland that lacks a .get function. Preserve any
// existing object but always ensure .get is callable so consumers
// never hit a TypeError on the inline-data fast path.
window.__dataIsland = window.__dataIsland || {};
- Strengthen __dataIsland defensive init: explicitly reset to {} when
  the existing value is null, undefined, a primitive (string, number,
  boolean, symbol), or any other non-object/non-function value.
  Previous `||` check let primitives through, so the subsequent .get
  assignment would silently fail in non-strict mode (or throw in strict
  mode) without ever installing a working accessor.
- Add two runtime tests: a parameterized test exercising string,
  number, boolean, and symbol pre-existing values; a separate test
  pinning the null case (typeof null === 'object' would defeat a
  naive object-only check).
- Fix recurring backtick-in-comment template-literal early-termination
  by replacing the inline-code backticks around 'null' with prose.

The repeating return-shape comment from Copilot has been addressed
across rounds 3, 4, and 5: the patched fetcher.load() returns a real
new Response(d.content, ...) when the constructor is available — with
url overridden via Object.defineProperty — and a Response-shaped
plain object (with .ok/.status/.url/.headers.get) as a fallback.
The bot continues to re-emit the original wording but the cited
return values (rdf.sym(s) / s) have not been in the source since
round 3.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated no new comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@melvincarvalho
melvincarvalho merged commit 9a41d3b into gh-pages May 2, 2026
4 checks passed
@melvincarvalho
melvincarvalho deleted the issue-346-round-trip-optimization branch May 2, 2026 04:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Round-trip optimization for HTML resource views via inline data island reader

2 participants