mirror of
https://github.com/got-feedBack/feedBack.git
synced 2026-09-11 07:14:30 +00:00
refactor(h3d-carve-2): extract Three.js loader to src/three-loader.js
Move the D-section (~21 lines) from screen.js factory scope to its own module.
Exports:
- loadThree() — memoised import() with local-vendor→CDN fallback; same body verbatim
- T (live let-binding) — updated to the Three.js namespace on first resolution
screen.js gains: import { loadThree, T } from './src/three-loader.js'
screen.js loses: let T = null; let threeLoadPromise = null; function loadThree()
T and loadThree remain accessible to the IIFE via module-scope closure. The live
let-binding means the IIFE reads the populated T after loadThree() resolves
without any call-site changes.
Panel-controls vm test: strip regex extended to consume all consecutive import
lines; T: null + loadThree stubs added to sandbox context.
Class-killer tests (8 new — highway_3d_three_loader.test.js):
- loadThree exported (mutation: rename/remove → import fails)
- T exported as mutable let (mutation: const → T=mod throws TypeError)
- T=mod in both .then handlers (mutation: remove both → T stays null)
- memoisation guard !threeLoadPromise (mutation: remove → race + duplicate loads)
- CDN fallback .catch chain (mutation: remove → deploy failures unrecoverable)
- threeLoadPromise reset on failure (mutation: remove → no retry possible)
- screen.js imports loadThree+T (wiring confirmed)
- IIFE no longer declares local T or threeLoadPromise (shadow defeated)
All class-killer mutations confirmed distinguishable before commit.
Base run (5e401af): 180/180 pass.
Post-cut run: 188/188 pass.
Command: node --test tests/js/highway_3d*.test.js plugins/highway_3d/tests/*.test.js
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014uZ169yfoFYArXz962g7KW
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
5e401afe87
commit
8ea123deaf
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"id": "highway_3d",
|
"id": "highway_3d",
|
||||||
"name": "3D Highway",
|
"name": "3D Highway",
|
||||||
"version": "3.37.0",
|
"version": "3.38.0",
|
||||||
"type": "visualization",
|
"type": "visualization",
|
||||||
"scriptType": "module",
|
"scriptType": "module",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
// changes.
|
// changes.
|
||||||
|
|
||||||
import { geoFretX, dZ, slideTrailEnd, camBaseDistU, camLowFretPullbackU, computeBPM, _makeGaussTex, RENDER_ORDER_LAYER_STACK, RENDER_ORDER_LAYER_INDEX, RENDER_ORDER_AT_Z_ZERO, RENDER_ORDER_FAR_CLAMP, renderOrderForLayerAtZ, _noteKey, lowerBoundT, hwyFirstRelevantFrettedTime, geoFretMid } from './src/geometry.js'; // h3d-carve-1b
|
import { geoFretX, dZ, slideTrailEnd, camBaseDistU, camLowFretPullbackU, computeBPM, _makeGaussTex, RENDER_ORDER_LAYER_STACK, RENDER_ORDER_LAYER_INDEX, RENDER_ORDER_AT_Z_ZERO, RENDER_ORDER_FAR_CLAMP, renderOrderForLayerAtZ, _noteKey, lowerBoundT, hwyFirstRelevantFrettedTime, geoFretMid } from './src/geometry.js'; // h3d-carve-1b
|
||||||
|
import { loadThree, T } from './src/three-loader.js'; // h3d-carve-2
|
||||||
|
|
||||||
(function () {
|
(function () {
|
||||||
'use strict';
|
'use strict';
|
||||||
@@ -1479,26 +1480,8 @@ import { geoFretX, dZ, slideTrailEnd, camBaseDistU, camLowFretPullbackU, compute
|
|||||||
|
|
||||||
// computeBPM, _makeGaussTex — moved to src/geometry.js (h3d-carve-1).
|
// computeBPM, _makeGaussTex — moved to src/geometry.js (h3d-carve-1).
|
||||||
|
|
||||||
/* ======================================================================
|
// T, loadThree — moved to src/three-loader.js (h3d-carve-2).
|
||||||
* Three.js module — lazily loaded, memoized
|
// T is a live-binding export; the IIFE reads the updated value after loadThree() resolves.
|
||||||
* ====================================================================== */
|
|
||||||
|
|
||||||
let T = null;
|
|
||||||
let threeLoadPromise = null;
|
|
||||||
function loadThree() {
|
|
||||||
if (!threeLoadPromise) {
|
|
||||||
threeLoadPromise = import(THREE_URL)
|
|
||||||
.then(mod => { T = mod; return mod; })
|
|
||||||
.catch(() => import(THREE_CDN)
|
|
||||||
.then(mod => { T = mod; return mod; })
|
|
||||||
.catch(e => {
|
|
||||||
console.error('[3D-Hwy] Three.js load failed:', e);
|
|
||||||
threeLoadPromise = null;
|
|
||||||
throw e;
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
return threeLoadPromise;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ======================================================================
|
/* ======================================================================
|
||||||
* Splitscreen helpers
|
* Splitscreen helpers
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
/**
|
||||||
|
* Three.js lazy loader — h3d-carve-2.
|
||||||
|
*
|
||||||
|
* Memoised singleton: the first `loadThree()` call kicks off the import;
|
||||||
|
* every subsequent call returns the same promise. `T` is a live-binding
|
||||||
|
* export so the IIFE in screen.js sees the updated reference after the
|
||||||
|
* promise resolves without any explicit getter call.
|
||||||
|
*
|
||||||
|
* Falls back to jsdelivr CDN when the local vendor copy is unavailable
|
||||||
|
* (air-gapped / static-file layout mismatch / dev origin).
|
||||||
|
*/
|
||||||
|
|
||||||
|
// ── URL constants (mirrors screen.js A-section; never vary at runtime) ──────
|
||||||
|
|
||||||
|
const THREE_URL = '/static/vendor/three/three.module.min.js';
|
||||||
|
const THREE_CDN = 'https://cdn.jsdelivr.net/npm/three@0.170.0/build/three.module.min.js';
|
||||||
|
|
||||||
|
// ── Memoised loader ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** Live binding — updated to the Three.js module namespace on first load. */
|
||||||
|
export let T = null;
|
||||||
|
|
||||||
|
let threeLoadPromise = null;
|
||||||
|
|
||||||
|
export function loadThree() {
|
||||||
|
if (!threeLoadPromise) {
|
||||||
|
threeLoadPromise = import(THREE_URL)
|
||||||
|
.then(mod => { T = mod; return mod; })
|
||||||
|
.catch(() => import(THREE_CDN)
|
||||||
|
.then(mod => { T = mod; return mod; })
|
||||||
|
.catch(e => {
|
||||||
|
console.error('[3D-Hwy] Three.js load failed:', e);
|
||||||
|
threeLoadPromise = null;
|
||||||
|
throw e;
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
return threeLoadPromise;
|
||||||
|
}
|
||||||
@@ -28,10 +28,10 @@ function loadHighway3dStatics() {
|
|||||||
1,
|
1,
|
||||||
'expected exactly one factory-registration anchor in screen.js',
|
'expected exactly one factory-registration anchor in screen.js',
|
||||||
);
|
);
|
||||||
// Since h3d-carve-1 screen.js starts with an ES module import statement.
|
// Since h3d-carve-1 screen.js starts with ES module import statements.
|
||||||
// vm.runInContext does not support static import — strip the import line
|
// vm.runInContext does not support static import — strip all leading import
|
||||||
// and provide stub implementations of the geometry exports in the sandbox.
|
// lines and provide stub implementations of the exports in the sandbox.
|
||||||
const stripped = src.replace(/^import\s+\{[^}]+\}\s+from\s+['"][^'"]+['"];\s*\/\/[^\n]*\n/m, '');
|
const stripped = src.replace(/^(import\s+\{[^}]+\}\s+from\s+['"][^'"]+['"];\s*\/\/[^\n]*\n)+/m, '');
|
||||||
const instrumented = stripped.replace(
|
const instrumented = stripped.replace(
|
||||||
ANCHOR,
|
ANCHOR,
|
||||||
`${ANCHOR}\n window.__h3dTestExports = { BG_DEFAULTS };`,
|
`${ANCHOR}\n window.__h3dTestExports = { BG_DEFAULTS };`,
|
||||||
@@ -72,6 +72,9 @@ function loadHighway3dStatics() {
|
|||||||
lowerBoundT: () => 0,
|
lowerBoundT: () => 0,
|
||||||
hwyFirstRelevantFrettedTime: () => null,
|
hwyFirstRelevantFrettedTime: () => null,
|
||||||
geoFretMid: (f, _uniform) => f * 0.1,
|
geoFretMid: (f, _uniform) => f * 0.1,
|
||||||
|
// h3d-carve-2: T and loadThree moved to src/three-loader.js.
|
||||||
|
T: null,
|
||||||
|
loadThree: () => Promise.resolve(),
|
||||||
};
|
};
|
||||||
vm.createContext(sandbox);
|
vm.createContext(sandbox);
|
||||||
vm.runInContext(instrumented, sandbox, { filename: SCREEN_JS });
|
vm.runInContext(instrumented, sandbox, { filename: SCREEN_JS });
|
||||||
|
|||||||
@@ -0,0 +1,88 @@
|
|||||||
|
// Class-killer for src/three-loader.js — h3d-carve-2.
|
||||||
|
//
|
||||||
|
// The loader is a memoised async import with a CDN fallback. Runtime calls
|
||||||
|
// import(url) which only resolves against a live server, so the behavioural
|
||||||
|
// contract is pinned by source-scan regex that name the concrete mutation
|
||||||
|
// each assertion catches.
|
||||||
|
//
|
||||||
|
// Source-scan is sufficient here: the loader's correctness depends entirely
|
||||||
|
// on its static structure (memoisation guard, T-assignment, CDN fallback)
|
||||||
|
// rather than on runtime values — the same pattern used for all other
|
||||||
|
// source-level tests in this suite.
|
||||||
|
|
||||||
|
const { test } = require('node:test');
|
||||||
|
const assert = require('node:assert/strict');
|
||||||
|
const fs = require('node:fs');
|
||||||
|
const path = require('node:path');
|
||||||
|
|
||||||
|
const LOADER_JS = path.join(__dirname, '..', '..', 'plugins', 'highway_3d', 'src', 'three-loader.js');
|
||||||
|
const SCREEN_JS = path.join(__dirname, '..', '..', 'plugins', 'highway_3d', 'screen.js');
|
||||||
|
|
||||||
|
let _loader;
|
||||||
|
function loader() {
|
||||||
|
if (!_loader) _loader = fs.readFileSync(LOADER_JS, 'utf8');
|
||||||
|
return _loader;
|
||||||
|
}
|
||||||
|
|
||||||
|
test('loadThree is exported from three-loader.js', () => {
|
||||||
|
// Mutation: rename or remove the export → the import in screen.js throws.
|
||||||
|
assert.match(loader(), /export\s+function\s+loadThree\s*\(\s*\)/,
|
||||||
|
'loadThree must be exported so screen.js can import it');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('T is exported as a live let-binding from three-loader.js', () => {
|
||||||
|
// Mutation: export const T — const bindings cannot be reassigned from within
|
||||||
|
// the module, so T = mod in loadThree() would throw a TypeError.
|
||||||
|
assert.match(loader(), /export\s+let\s+T\s*=\s*null/,
|
||||||
|
'T must be exported as a mutable let-binding so the .then handler can update it');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('loadThree assigns T = mod in both the primary and CDN .then handlers', () => {
|
||||||
|
// Mutation: remove all T = mod assignments — T stays null forever; all Three.js
|
||||||
|
// calls throw. Two assignments exist: primary load and CDN fallback.
|
||||||
|
const matches = loader().match(/T\s*=\s*mod\s*;/g) || [];
|
||||||
|
assert.ok(matches.length >= 2,
|
||||||
|
'T = mod must appear in both the primary and CDN .then handlers (found ' + matches.length + ')');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('loadThree memoises the promise — returns existing promise on repeated calls', () => {
|
||||||
|
// Mutation: remove the !threeLoadPromise guard — a new promise is kicked off on
|
||||||
|
// every call, racing against previous loads and resetting T on each resolution.
|
||||||
|
assert.match(loader(), /if\s*\(\s*!threeLoadPromise\s*\)/,
|
||||||
|
'memoisation guard must prevent duplicate simultaneous import() calls');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('loadThree has a CDN fallback for the local vendor copy', () => {
|
||||||
|
// Mutation: remove the .catch(() => import(THREE_CDN) chain — offline / mis-routed
|
||||||
|
// deploys that fail to reach /static/vendor/three/ get no fallback and throw.
|
||||||
|
assert.match(loader(), /\.catch\s*\(\s*\(\s*\)\s*=>\s*import\s*\(\s*THREE_CDN\s*\)/,
|
||||||
|
'CDN fallback must kick in when the local vendor copy is unavailable');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('loadThree resets threeLoadPromise to null on total failure', () => {
|
||||||
|
// Mutation: remove threeLoadPromise = null in the final catch — a failed load
|
||||||
|
// permanently memoises the rejected promise; a page reload recovers but a plugin
|
||||||
|
// re-init (same session) can never retry the import.
|
||||||
|
assert.match(loader(), /threeLoadPromise\s*=\s*null/,
|
||||||
|
'failed load must reset threeLoadPromise so a retry can succeed');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('screen.js imports loadThree and T from three-loader.js', () => {
|
||||||
|
// Confirms the import line is present and the live-binding is wired.
|
||||||
|
const src = fs.readFileSync(SCREEN_JS, 'utf8');
|
||||||
|
assert.match(src,
|
||||||
|
/import\s+\{\s*loadThree\s*,\s*T\s*\}\s*from\s*['"]\.\/src\/three-loader\.js['"]/,
|
||||||
|
'screen.js must import both loadThree and T from the loader module');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('screen.js IIFE no longer declares local let T or let threeLoadPromise', () => {
|
||||||
|
// Mutation: leave the old local declarations in place — the IIFE's local T shadows
|
||||||
|
// the live-binding import so T is always null inside the factory.
|
||||||
|
const src = fs.readFileSync(SCREEN_JS, 'utf8');
|
||||||
|
// Strip the import lines at the top of the file before searching the IIFE body.
|
||||||
|
const iife = src.replace(/^import\s+.*?\n/gm, '');
|
||||||
|
assert.doesNotMatch(iife, /\blet\s+T\s*=\s*null\s*;/,
|
||||||
|
'IIFE must not redeclare T — the local shadow would defeat the live-binding export');
|
||||||
|
assert.doesNotMatch(iife, /\blet\s+threeLoadPromise\s*=\s*null\s*;/,
|
||||||
|
'IIFE must not redeclare threeLoadPromise — it belongs to the loader module now');
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user