@descix/frqtl-node 0.6.0-p0.2 → 0.6.0-p4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +2 -2
- package/README.md +8 -4
- package/dist/dist-manifest.json +70 -62
- package/dist/eyes/index.js +5 -2
- package/dist/frqtl/assets/vendor/acorn.LICENSE +21 -0
- package/dist/frqtl/assets/vendor/acorn.PINNED.json +9 -0
- package/dist/frqtl/assets/vendor/acorn.mjs +6266 -0
- package/dist/frqtl/compat/renderer/lo-drawn-center.js +41 -0
- package/dist/frqtl/experiments/_shared/boot-failure.js +60 -0
- package/dist/frqtl/objects/FrqtlApp.js +136 -30
- package/dist/frqtl/objects/ObjectFactory.js +4 -5
- package/dist/frqtl/objects/RecipeContext.js +617 -9
- package/dist/frqtl/objects/instruments/hosted-eyes.js +268 -57
- package/dist/frqtl/objects/objects/QuantumEmitter.js +1 -1
- package/dist/frqtl/objects/record/Record.js +326 -0
- package/dist/frqtl/objects/record/open-record.js +22 -0
- package/dist/frqtl/objects/schema/index.js +72 -0
- package/dist/frqtl/parts/DetectorWall.js +1 -1
- package/dist/frqtl/parts/FrqtlControlsPanel.js +4 -2
- package/dist/frqtl/parts/FrqtlVision.js +21 -6
- package/dist/frqtl/parts/Wall.js +2 -0
- package/dist/frqtl/scenes/absorbing-wall-3d.js +18 -14
- package/dist/frqtl/scenes/absorbing-wall.js +18 -14
- package/dist/frqtl/scenes/atomic-model.js +3 -0
- package/dist/frqtl/scenes/big-bang.js +3 -0
- package/dist/frqtl/scenes/blackbody.js +14 -11
- package/dist/frqtl/scenes/circuit-sat.js +7 -4
- package/dist/frqtl/scenes/collision-matrix-3d.js +3 -0
- package/dist/frqtl/scenes/counting-wall-3d.js +19 -15
- package/dist/frqtl/scenes/counting-wall.js +19 -15
- package/dist/frqtl/scenes/double-slit.js +3 -0
- package/dist/frqtl/scenes/dse-one-emitter.js +64 -58
- package/dist/frqtl/scenes/dse-one-slit.js +59 -55
- package/dist/frqtl/scenes/dse-photoelectric.js +28 -24
- package/dist/frqtl/scenes/dse-tunnel-rebuild.js +124 -99
- package/dist/frqtl/scenes/dse-two-emitters.js +64 -58
- package/dist/frqtl/scenes/dse-two-slits.js +69 -65
- package/dist/frqtl/scenes/empty.js +3 -0
- package/dist/frqtl/scenes/laser-buildup.js +23 -16
- package/dist/frqtl/scenes/mirror-3d.js +31 -27
- package/dist/frqtl/scenes/mirror-route.js +4 -1
- package/dist/frqtl/scenes/mirror.js +24 -20
- package/dist/frqtl/scenes/omni-3d.js +7 -4
- package/dist/frqtl/scenes/omni-source.js +7 -4
- package/dist/frqtl/scenes/self-interference.js +13 -10
- package/dist/frqtl/scenes/smoking-gun.js +15 -11
- package/dist/frqtl/scenes/straight-walk.js +7 -4
- package/dist/frqtl/scenes/sun.js +11 -8
- package/dist/frqtl/scenes/two-source.js +22 -17
- package/dist/frqtl/scenes/wave.js +10 -7
- package/dist/shim/bundle/index.js +112 -96
- package/dist/shim/bundle/indexHtml.js +66 -39
- package/dist/shim/engine/router.js +2 -2
- package/dist/shim/index.js +1 -1
- package/dist/shim/notebook/format.js +1 -1
- package/dist/shim/notebook/store.js +2 -3
- package/dist/shim/page-params/index.js +2 -2
- package/dist/shim/project/index.js +20 -5
- package/dist/shim/project/link.js +182 -37
- package/dist/shim/project/manifest.js +151 -0
- package/dist/shim/project/parts-module.js +67 -0
- package/dist/shim/project/parts-writer.js +457 -0
- package/dist/shim/project/record-format.js +345 -0
- package/dist/shim/project/specifiers.js +41 -13
- package/dist/shim/registry/index.js +1 -1
- package/dist/shim/registry/scene-source.js +44 -30
- package/dist/shim/registry/walkthrough.js +13 -12
- package/dist/shim/render-state/apply.js +11 -14
- package/dist/shim/storage/codec-chains.js +88 -0
- package/dist/shim/storage/folders.js +205 -0
- package/dist/shim/storage/index.js +64 -8
- package/dist/shim/storage/make-namespace-store.js +5 -5
- package/llms.txt +2 -2
- package/package.json +1 -1
- package/dist/shim/bundle/frqnb.js +0 -112
- package/dist/shim/bundle/manifest.js +0 -95
- package/dist/shim/storage/modules.js +0 -249
- package/dist/shim/storage/projects.js +0 -150
|
@@ -66,10 +66,11 @@
|
|
|
66
66
|
*
|
|
67
67
|
* A SceneRecord alone cannot open a host: it is identity plus reproduction pins, with no
|
|
68
68
|
* scene spec in it. So the API takes the PAIR — record + `{kind:'project', files, entry}`, the
|
|
69
|
-
* project whose entry's default export is `buildExperiment(exp)` (a one-string sketch, such as
|
|
70
|
-
*
|
|
69
|
+
* project whose entry's default export is `buildExperiment(exp)` (a one-string sketch, such as a
|
|
70
|
+
* shipped scene file, is `{ files: { 'sketch.js': text }, entry: 'sketch.js' }`). The
|
|
71
71
|
* page links it through its one door and stamps the project's identity (projectIdentity) as the
|
|
72
|
-
* record's `sourceSha256`; this process computes the same identity from the same
|
|
72
|
+
* record's `sourceSha256`; this process computes the same identity from the same linked set
|
|
73
|
+
* (lib/shim/project/link.js linkedFiles — frqtl.json and the module files; a README rides unhashed), and the
|
|
73
74
|
* open-time record check holds the two equal. A caller without one is REPLAY-grade
|
|
74
75
|
* (an ad-hoc tick-fn scene, a live-edited scene, a bare raster row) and is REFUSED in
|
|
75
76
|
* RENDERER_CONTRACT §10.3's own vocabulary: there is nothing to regenerate FROM, and a
|
|
@@ -132,6 +133,8 @@ import { FrqtlCaptureHost } from './capture-host.js';
|
|
|
132
133
|
import { FrqtlVision } from '../../parts/FrqtlVision.js';
|
|
133
134
|
import { FrqtlCapture } from './capture.js';
|
|
134
135
|
import { projectIdentity, projectBytes } from '../../../shim/project/identity.js';
|
|
136
|
+
import { linkedFiles, entryOf } from '../../../shim/project/link.js';
|
|
137
|
+
import { libPathOf } from '../../../shim/project/specifiers.js';
|
|
135
138
|
|
|
136
139
|
|
|
137
140
|
/** The contracts this instrument consumes, by static import (it is a node-only module). */
|
|
@@ -145,22 +148,29 @@ import { projectIdentity, projectBytes } from '../../../shim/project/identity.js
|
|
|
145
148
|
}
|
|
146
149
|
|
|
147
150
|
/**
|
|
148
|
-
* The URL, AS THE PAGE SEES IT, of a module beside this file.
|
|
149
|
-
*
|
|
150
|
-
*
|
|
151
|
-
*
|
|
152
|
-
*
|
|
151
|
+
* The URL, AS THE PAGE SEES IT, of a module beside this file. MODULES sit at their
|
|
152
|
+
* lib-relative paths in lib/ and in every dist, so a module's lib path is its path from this
|
|
153
|
+
* tree's lib root. The PAGE does not: the dev mount serves it inside the lib tree, a dist
|
|
154
|
+
* re-anchors it (`<dist>/experiments/empty/`, the graph beside or under a build directory),
|
|
155
|
+
* so where the page sits says nothing about where its modules are. The page's own import map
|
|
156
|
+
* does — every row is its lib root plus a lib path (lib/shim/project/specifiers.js
|
|
157
|
+
* `importMapFor`, rendered for each placement) — so the lib root is asked of the page
|
|
158
|
+
* (`_IN_PAGE_LIB_ROOT` on `PAGE_ANCHOR`) and each module's lib path resolved against it.
|
|
159
|
+
* The in-page routines import the PAGE's module instances from these URLs — never a copy.
|
|
160
|
+
* @private
|
|
153
161
|
*/
|
|
154
|
-
var
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
162
|
+
var LIB_ROOT_DIR = fileURLToPath(new URL('../../../', import.meta.url));
|
|
163
|
+
var PAGE_ANCHOR_SPECIFIER = '@descix/frqtl-web/parts';
|
|
164
|
+
var PAGE_ANCHOR = { specifier: PAGE_ANCHOR_SPECIFIER, libPath: libPathOf(PAGE_ANCHOR_SPECIFIER) };
|
|
165
|
+
function _pageModuleUrl(pageLibRoot, moduleUrl) {
|
|
166
|
+
var rel = path.relative(LIB_ROOT_DIR, fileURLToPath(moduleUrl)).split(path.sep).join('/');
|
|
167
|
+
return new URL(rel, pageLibRoot).href;
|
|
158
168
|
}
|
|
159
|
-
/** The modules the capture-host KIND's in-page routines import, for
|
|
160
|
-
function _inPageModules(
|
|
169
|
+
/** The modules the capture-host KIND's in-page routines import, for a page whose lib root is `pageLibRoot`. @private */
|
|
170
|
+
function _inPageModules(pageLibRoot) {
|
|
161
171
|
return {
|
|
162
|
-
captureHost: _pageModuleUrl(
|
|
163
|
-
captureEnvelope: _pageModuleUrl(
|
|
172
|
+
captureHost: _pageModuleUrl(pageLibRoot, import.meta.resolve('./capture-host.js')),
|
|
173
|
+
captureEnvelope: _pageModuleUrl(pageLibRoot, import.meta.resolve('../../../shim/render/capture-envelope.js'))
|
|
164
174
|
};
|
|
165
175
|
}
|
|
166
176
|
|
|
@@ -183,7 +193,8 @@ import { projectIdentity, projectBytes } from '../../../shim/project/identity.js
|
|
|
183
193
|
// ── The REGENERATE gate ───────────────────────────────────────────────────
|
|
184
194
|
|
|
185
195
|
/**
|
|
186
|
-
* assertHostable({sceneRecord, sceneSource}) — throw-or-return, in §10.3's vocabulary.
|
|
196
|
+
* assertHostable({sceneRecord, sceneSource}) — throw-or-return the entry, in §10.3's vocabulary. The entry is
|
|
197
|
+
* THE entry rule's answer (link.js entryOf): the entry frqtl.json names, else sceneSource.entry.
|
|
187
198
|
* @private
|
|
188
199
|
*/
|
|
189
200
|
function _assertHostable(opts) {
|
|
@@ -205,18 +216,20 @@ import { projectIdentity, projectBytes } from '../../../shim/project/identity.js
|
|
|
205
216
|
'(more files beside it for a project with forked parts). (FAIL-LOUD.)');
|
|
206
217
|
}
|
|
207
218
|
if (!src || typeof src !== 'object' || src.kind !== 'project' ||
|
|
208
|
-
!src.files || typeof src.files !== 'object' || Object.keys(src.files).length === 0
|
|
209
|
-
typeof src.entry !== 'string' || !src.entry) {
|
|
219
|
+
!src.files || typeof src.files !== 'object' || Object.keys(src.files).length === 0) {
|
|
210
220
|
throw new Error('[FrqtlHostedEyes.open] this scene is REPLAY-grade, not ' +
|
|
211
221
|
'REGENERATE-grade: hosting re-RUNS the scene in another process, so a ' +
|
|
212
222
|
'SceneRecord alone is not enough (it is identity + reproduction pins, with no ' +
|
|
213
|
-
'scene spec in it). Pass the pair — sceneSource: { kind: "project", files, entry }
|
|
223
|
+
'scene spec in it). Pass the pair — sceneSource: { kind: "project", files, entry? } (entry optional when ' +
|
|
224
|
+
'files carries the frqtl.json that names it) — ' +
|
|
214
225
|
'where the entry\'s default export is buildExperiment(exp) and the canonical empty ' +
|
|
215
226
|
'page links it. An ad-hoc tick-fn scene, a live-edited scene or a ' +
|
|
216
227
|
'bare raster row has nothing to regenerate FROM, and a silently approximate ' +
|
|
217
228
|
're-run is the one failure a content-addressed corpus cannot detect afterwards. ' +
|
|
218
229
|
'(FAIL-LOUD — see RENDERER_CONTRACT.md §10.3.)');
|
|
219
230
|
}
|
|
231
|
+
// THE entry rule (link.js entryOf): frqtl.json's entry, a given one equal to it, else the given one.
|
|
232
|
+
return entryOf(src.files, src.entry);
|
|
220
233
|
}
|
|
221
234
|
|
|
222
235
|
/**
|
|
@@ -362,11 +375,11 @@ import { projectIdentity, projectBytes } from '../../../shim/project/identity.js
|
|
|
362
375
|
// ── The boot wait: every way it can fail is a NAMED refusal ─────────────────
|
|
363
376
|
|
|
364
377
|
/**
|
|
365
|
-
* Where a published dist
|
|
366
|
-
*
|
|
378
|
+
* Where a published dist serves the one page — under the dist's own URL, which may be an
|
|
379
|
+
* origin root or a sub-path, so it is named, not computed from `url`. @private
|
|
367
380
|
*/
|
|
368
|
-
function _distEmptyPage(
|
|
369
|
-
return
|
|
381
|
+
function _distEmptyPage() {
|
|
382
|
+
return '<dist>/experiments/empty/index.html';
|
|
370
383
|
}
|
|
371
384
|
|
|
372
385
|
/** D — the URL did not serve (an HTTP status other than 2xx, or a navigation error). @private */
|
|
@@ -374,7 +387,7 @@ import { projectIdentity, projectBytes } from '../../../shim/project/identity.js
|
|
|
374
387
|
return new Error('[FrqtlHostedEyes.open] ' + url + ' did not serve: ' + what + '. That is ' +
|
|
375
388
|
'a deployment fact, not a scene problem — the hosted eyes boot ONLY the canonical empty ' +
|
|
376
389
|
'experiment page lib/frqtl/experiments/empty/index.html as your deployment serves it ' +
|
|
377
|
-
'(a published dist
|
|
390
|
+
'(a published dist serves it at ' + _distEmptyPage() + ', <dist> being the URL the dist is served at); point ' +
|
|
378
391
|
'`pageUrl` at it, or serve it. Nothing was booted. (FAIL-LOUD.)');
|
|
379
392
|
}
|
|
380
393
|
|
|
@@ -383,7 +396,7 @@ import { projectIdentity, projectBytes } from '../../../shim/project/identity.js
|
|
|
383
396
|
return new Error('[FrqtlHostedEyes.open] ' + url + ' is not the canonical empty experiment ' +
|
|
384
397
|
'page: it loaded (HTTP ' + status + ') and defines no window.FrqtlPage. The hosted eyes ' +
|
|
385
398
|
'boot ONLY lib/frqtl/experiments/empty/index.html (in a published dist: ' +
|
|
386
|
-
_distEmptyPage(
|
|
399
|
+
_distEmptyPage() + ') and apply your scene into it. They never host an app page, the ' +
|
|
387
400
|
'IDE, or a page of your own: a page that boots and edits its own scene cannot be ' +
|
|
388
401
|
'regenerated from {sceneRecord, sceneSource}. To see your own page, capture IN it ' +
|
|
389
402
|
'through FrqtlVision (the in-page face). To drive it from node, use your own browser; ' +
|
|
@@ -470,16 +483,10 @@ import { projectIdentity, projectBytes } from '../../../shim/project/identity.js
|
|
|
470
483
|
}
|
|
471
484
|
applied = window.FrqtlPage.applyRenderState(args.renderState);
|
|
472
485
|
}
|
|
473
|
-
// ESTABLISH THE CLOCK with one step, deliberately
|
|
474
|
-
//
|
|
475
|
-
//
|
|
476
|
-
//
|
|
477
|
-
// read as "tick 10"). Every later read here is exact BECAUSE this process is the only
|
|
478
|
-
// stepper (the RAF loop is stopped above), so the fix is to make the first reading a
|
|
479
|
-
// real one rather than to trust a cache the re-init left behind. It costs nothing the
|
|
480
|
-
// contract was not already charging: a capture before the first tick is REFUSED
|
|
481
|
-
// anyway ("no tick has run yet" — never an empty buffer), so tick 1 is the earliest
|
|
482
|
-
// capturable coordinate of any scene.
|
|
486
|
+
// ESTABLISH THE CLOCK with one step, deliberately: applying a recipe begins a world that has not stepped
|
|
487
|
+
// (runner.worldTick 0, no TickData yet), and a capture before the first tick is REFUSED ("no tick has run yet"
|
|
488
|
+
// — never an empty buffer), so tick 1 is the earliest capturable coordinate of any scene. Every later read is
|
|
489
|
+
// exact because this process is the only stepper (the RAF loop is stopped above).
|
|
483
490
|
r.step();
|
|
484
491
|
return { renderState: applied };
|
|
485
492
|
};
|
|
@@ -545,8 +552,8 @@ import { projectIdentity, projectBytes } from '../../../shim/project/identity.js
|
|
|
545
552
|
return;
|
|
546
553
|
}
|
|
547
554
|
try {
|
|
548
|
-
|
|
549
|
-
resolve(
|
|
555
|
+
r.step();
|
|
556
|
+
resolve(r.worldTick);
|
|
550
557
|
} catch (e) { reject(e); }
|
|
551
558
|
})();
|
|
552
559
|
});
|
|
@@ -554,8 +561,35 @@ import { projectIdentity, projectBytes } from '../../../shim/project/identity.js
|
|
|
554
561
|
|
|
555
562
|
/** The current tick, read (never advanced). */
|
|
556
563
|
var _IN_PAGE_TICK = function () {
|
|
557
|
-
var
|
|
558
|
-
|
|
564
|
+
var r = window.FrqtlPage.runner;
|
|
565
|
+
if (!r) { throw new Error('[FrqtlHostedEyes/in-page] there is no runner to read a tick from (FrqtlPage.runner). (FAIL-LOUD.)'); }
|
|
566
|
+
return r.worldTick;
|
|
567
|
+
};
|
|
568
|
+
|
|
569
|
+
/**
|
|
570
|
+
* The page's lib root, resolved by the PAGE's import map: `anchor.specifier` resolves to the lib
|
|
571
|
+
* root plus `anchor.libPath`. A `blob:` module asks, because `import.meta.resolve` is the
|
|
572
|
+
* browser's own resolution against the document's map (the link site's blob modules resolve
|
|
573
|
+
* their imports the same way).
|
|
574
|
+
*/
|
|
575
|
+
var _IN_PAGE_LIB_ROOT = async function (anchor) {
|
|
576
|
+
var url = URL.createObjectURL(new Blob(['export default import.meta.resolve(' +
|
|
577
|
+
JSON.stringify(anchor.specifier) + ');'], { type: 'text/javascript' }));
|
|
578
|
+
var resolved;
|
|
579
|
+
try { resolved = (await import(url)).default; }
|
|
580
|
+
catch (e) {
|
|
581
|
+
throw new Error('[FrqtlHostedEyes/in-page] this page\'s import map does not resolve ' +
|
|
582
|
+
JSON.stringify(anchor.specifier) + ' (' + ((e && e.message) || String(e)) + ') — the canonical ' +
|
|
583
|
+
'empty page carries the specifier table\'s import map; nothing can locate its modules ' +
|
|
584
|
+
'without it. (FAIL-LOUD.)');
|
|
585
|
+
}
|
|
586
|
+
finally { URL.revokeObjectURL(url); }
|
|
587
|
+
if (typeof resolved !== 'string' || resolved.slice(-anchor.libPath.length) !== anchor.libPath) {
|
|
588
|
+
throw new Error('[FrqtlHostedEyes/in-page] ' + JSON.stringify(anchor.specifier) + ' resolved to ' +
|
|
589
|
+
JSON.stringify(resolved) + ', which does not end in its lib path ' + JSON.stringify(anchor.libPath) +
|
|
590
|
+
' — the page\'s import map is not the specifier table\'s rendering. (FAIL-LOUD.)');
|
|
591
|
+
}
|
|
592
|
+
return resolved.slice(0, resolved.length - anchor.libPath.length);
|
|
559
593
|
};
|
|
560
594
|
|
|
561
595
|
/** Point the ONE camera and redraw — pose is a still coordinate, set before capture. */
|
|
@@ -572,6 +606,99 @@ import { projectIdentity, projectBytes } from '../../../shim/project/identity.js
|
|
|
572
606
|
return cam.getOrbit();
|
|
573
607
|
};
|
|
574
608
|
|
|
609
|
+
// ── The Record door, in the page (lib/frqtl/objects/record/Record.js through FrqtlPage.experiment) ──
|
|
610
|
+
// The page's facade owns the Record; these routines only forward to it, so the folder a node host gets
|
|
611
|
+
// is the one the page's own door wrote (one implementation, every host). The open Record and a film
|
|
612
|
+
// in flight are parked on the page under ONE name, `__frqtlHostedRecord`.
|
|
613
|
+
|
|
614
|
+
/** exp.startRecord() on the page's facade; parks the Record. */
|
|
615
|
+
var _IN_PAGE_RECORD_START = async function () {
|
|
616
|
+
var exp = window.FrqtlPage.experiment;
|
|
617
|
+
if (!exp || typeof exp.startRecord !== 'function') {
|
|
618
|
+
throw new Error('[FrqtlHostedEyes/in-page] this page has no FrqtlPage.experiment facade to record.');
|
|
619
|
+
}
|
|
620
|
+
var rec;
|
|
621
|
+
// A refusal crosses the page boundary as data, so its err.record reaches node (page.evaluate keeps only a message).
|
|
622
|
+
try { rec = await exp.startRecord(); }
|
|
623
|
+
catch (e) { if (e && e.record) { return { refused: { message: e.message, record: e.record } }; } throw e; }
|
|
624
|
+
window.__frqtlHostedRecord = { rec: rec, film: null };
|
|
625
|
+
return { tick: rec.tick };
|
|
626
|
+
};
|
|
627
|
+
|
|
628
|
+
/** rec.film(opts) begins: its first segment subscribes before this returns (the drive gate waits on it). */
|
|
629
|
+
var _IN_PAGE_RECORD_FILM_BEGIN = function (opts) {
|
|
630
|
+
var p = window.__frqtlHostedRecord;
|
|
631
|
+
if (!p || !p.rec) { throw new Error('[FrqtlHostedEyes/in-page] no Record is open — eyes.record.start() first.'); }
|
|
632
|
+
if (p.film) { throw new Error('[FrqtlHostedEyes/in-page] a film is already running — await it first.'); }
|
|
633
|
+
p.film = p.rec.film(opts);
|
|
634
|
+
p.film.then(null, function () {});
|
|
635
|
+
return true;
|
|
636
|
+
};
|
|
637
|
+
|
|
638
|
+
/** The film's clip spans, when it ends. */
|
|
639
|
+
var _IN_PAGE_RECORD_FILM_END = function () {
|
|
640
|
+
var p = window.__frqtlHostedRecord;
|
|
641
|
+
var f = p && p.film;
|
|
642
|
+
if (!f) { throw new Error('[FrqtlHostedEyes/in-page] no film is running.'); }
|
|
643
|
+
return f.then(function (spans) { p.film = null; return spans; }, function (e) { p.film = null; throw e; });
|
|
644
|
+
};
|
|
645
|
+
|
|
646
|
+
/** rec.stop() → the folder, each clip's bytes as base64 (the one encoder, capture-envelope.js). */
|
|
647
|
+
var _IN_PAGE_RECORD_STOP = async function (args) {
|
|
648
|
+
var p = window.__frqtlHostedRecord;
|
|
649
|
+
if (!p || !p.rec) { throw new Error('[FrqtlHostedEyes/in-page] no Record is open — eyes.record.start() first.'); }
|
|
650
|
+
var CE = await import(args.modules.captureEnvelope);
|
|
651
|
+
var out;
|
|
652
|
+
// A refused stop() leaves the Record open (still parked); its err.record crosses as data.
|
|
653
|
+
try { out = await p.rec.stop(); }
|
|
654
|
+
catch (e) { if (e && e.record) { return { refused: { message: e.message, record: e.record } }; } throw e; }
|
|
655
|
+
window.__frqtlHostedRecord = null;
|
|
656
|
+
if (out.empty) { return { empty: true }; }
|
|
657
|
+
var files = {};
|
|
658
|
+
Object.keys(out.files).forEach(function (k) {
|
|
659
|
+
var v = out.files[k];
|
|
660
|
+
files[k] = typeof v === 'string' ? { text: v } : { base64: CE.bytesToBase64(v) };
|
|
661
|
+
});
|
|
662
|
+
return { files: files, record: out.record, clips: out.clips };
|
|
663
|
+
};
|
|
664
|
+
|
|
665
|
+
/** One act through the page's own doors: toSketch({by}), setDeclared(name, patch, {by}), placeDeclaredAt(name, point). */
|
|
666
|
+
var _IN_PAGE_ACT = async function (args) {
|
|
667
|
+
var exp = window.FrqtlPage.experiment;
|
|
668
|
+
var r = window.FrqtlPage.runner;
|
|
669
|
+
if (args.door === 'toSketch') { return exp.toSketch(args.opts); }
|
|
670
|
+
if (args.door === 'setDeclared') { return exp.setDeclared(args.name, args.patch, args.opts); }
|
|
671
|
+
if (args.door === 'placeDeclaredAt') {
|
|
672
|
+
var d = r.declared.find(function (o) { return o.name === args.name; });
|
|
673
|
+
if (!d) { throw new Error('[FrqtlHostedEyes/in-page] no declared object is named ' + JSON.stringify(args.name) + '.'); }
|
|
674
|
+
var res = r.placeDeclaredAt(d, args.point);
|
|
675
|
+
r.repaint();
|
|
676
|
+
return res;
|
|
677
|
+
}
|
|
678
|
+
throw new Error('[FrqtlHostedEyes/in-page] unknown act door ' + JSON.stringify(args.door) + '.');
|
|
679
|
+
};
|
|
680
|
+
|
|
681
|
+
/** A page refusal `{ refused: { message, record } }` → a thrown Error carrying `.record`, as in a page. @private */
|
|
682
|
+
function _rethrowRefused(out) {
|
|
683
|
+
if (out && out.refused) {
|
|
684
|
+
var e = new Error(out.refused.message);
|
|
685
|
+
e.record = out.refused.record;
|
|
686
|
+
throw e;
|
|
687
|
+
}
|
|
688
|
+
return out;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
/** A page folder (text | base64 per path) → { path: string | Uint8Array }. @private */
|
|
692
|
+
function _folderFromPage(out) {
|
|
693
|
+
if (out.empty) { return { empty: true }; }
|
|
694
|
+
var files = {};
|
|
695
|
+
Object.keys(out.files).forEach(function (k) {
|
|
696
|
+
var v = out.files[k];
|
|
697
|
+
files[k] = (v.text !== undefined) ? v.text : new Uint8Array(Buffer.from(v.base64, 'base64'));
|
|
698
|
+
});
|
|
699
|
+
return { files: files, record: out.record, clips: out.clips };
|
|
700
|
+
}
|
|
701
|
+
|
|
575
702
|
// ── open() ────────────────────────────────────────────────────────────────
|
|
576
703
|
|
|
577
704
|
/**
|
|
@@ -583,8 +710,9 @@ import { projectIdentity, projectBytes } from '../../../shim/project/identity.js
|
|
|
583
710
|
* world the source's co-init call built). Supplying any of them is an EXPECTATION, checked
|
|
584
711
|
* after boot and THROWN on mismatch — never a request; a stated bound that is not a
|
|
585
712
|
* positive integer is refused before a browser is acquired.
|
|
586
|
-
* @param {{kind:'project', files:Object<string,string>, entry
|
|
587
|
-
* REGENERATE-grade scene: a project, linked in the page by its one door.
|
|
713
|
+
* @param {{kind:'project', files:Object<string,string>, entry?:string}} opts.sceneSource — the
|
|
714
|
+
* REGENERATE-grade scene: a project, linked in the page by its one door. `entry` is optional when
|
|
715
|
+
* `files` carries the frqtl.json that names it (a different one is refused).
|
|
588
716
|
* @param {object} [opts.renderState] — the declared LOOK, ferried: a renderState bag (for a
|
|
589
717
|
* rung, `effectiveRenderState(rung, mode)`; or a saved project's bag). Its `renderer` boots
|
|
590
718
|
* the page; its rows, camera and backdrop are applied by `FrqtlPage.applyRenderState` before
|
|
@@ -595,7 +723,8 @@ import { projectIdentity, projectBytes } from '../../../shim/project/identity.js
|
|
|
595
723
|
* `setPose` after open overrides its camera. Omit it for a scene's PLAIN render.
|
|
596
724
|
* @param {string} opts.pageUrl — the canonical empty experiment page in YOUR deployment
|
|
597
725
|
* (dev server: `<origin>/frqtl/experiments/empty/index.html`; published dist:
|
|
598
|
-
* `<
|
|
726
|
+
* `<dist>/experiments/empty/index.html`, where `<dist>` is the dist's URL — an origin root
|
|
727
|
+
* or a sub-path). Required rather than guessed: the address of
|
|
599
728
|
* a deployment is not something a library may invent.
|
|
600
729
|
* @param {object} opts.browserType — a Playwright browser type (`chromium`). The
|
|
601
730
|
* `@descix/frqtl-node/eyes` entry supplies the packaged default; passing your own IS the
|
|
@@ -619,14 +748,14 @@ import { projectIdentity, projectBytes } from '../../../shim/project/identity.js
|
|
|
619
748
|
/** @private — the body; see `open` for the contract. */
|
|
620
749
|
function _open(opts) {
|
|
621
750
|
var C = _contracts();
|
|
622
|
-
_assertHostable(opts);
|
|
751
|
+
var entry = _assertHostable(opts);
|
|
623
752
|
_assertBoundShape(opts.sceneRecord, C.RS);
|
|
624
753
|
var declared = _declaredRenderState(opts, opts.sceneRecord, C.RST);
|
|
625
754
|
if (!opts.pageUrl || typeof opts.pageUrl !== 'string') {
|
|
626
755
|
throw new Error('[FrqtlHostedEyes.open] `pageUrl` is REQUIRED: the URL of the ' +
|
|
627
756
|
'CANONICAL empty experiment page (lib/frqtl/experiments/empty/index.html) in your ' +
|
|
628
757
|
'deployment — a dev server exposes it at its /frqtl mount, a published dist at ' +
|
|
629
|
-
'<
|
|
758
|
+
'<dist>/experiments/empty/index.html (<dist> = the URL the dist is served at). A library may not invent the address of ' +
|
|
630
759
|
'your deployment, and it must not ship a second copy of that page. (FAIL-LOUD.)');
|
|
631
760
|
}
|
|
632
761
|
if (!opts.browserType || typeof opts.browserType.launch !== 'function') {
|
|
@@ -641,7 +770,7 @@ import { projectIdentity, projectBytes } from '../../../shim/project/identity.js
|
|
|
641
770
|
var sourceRef = (record.sourceRef === undefined || record.sourceRef === null)
|
|
642
771
|
? null : String(record.sourceRef);
|
|
643
772
|
|
|
644
|
-
var browser = null, page = null;
|
|
773
|
+
var browser = null, page = null, modules = null;
|
|
645
774
|
return Promise.resolve(opts.browserType.launch({
|
|
646
775
|
headless: true,
|
|
647
776
|
args: Array.isArray(opts.launchArgs) ? opts.launchArgs : _defaultLaunchArgs()
|
|
@@ -690,7 +819,7 @@ import { projectIdentity, projectBytes } from '../../../shim/project/identity.js
|
|
|
690
819
|
})
|
|
691
820
|
.then(function () {
|
|
692
821
|
return page.evaluate(_IN_PAGE_APPLY,
|
|
693
|
-
{ files: opts.sceneSource.files, entry:
|
|
822
|
+
{ files: opts.sceneSource.files, entry: entry, sourceRef: sourceRef, renderState: declared });
|
|
694
823
|
})
|
|
695
824
|
.then(function (applied) {
|
|
696
825
|
return page.evaluate(_IN_PAGE_DISPLAY).then(function (disp) {
|
|
@@ -735,6 +864,11 @@ import { projectIdentity, projectBytes } from '../../../shim/project/identity.js
|
|
|
735
864
|
_assertLanded(boot.renderState);
|
|
736
865
|
});
|
|
737
866
|
}).then(function () {
|
|
867
|
+
// Asked of the FINAL page (after any display reload), once: the in-page routines import
|
|
868
|
+
// its module instances from these URLs.
|
|
869
|
+
return page.evaluate(_IN_PAGE_LIB_ROOT, PAGE_ANCHOR);
|
|
870
|
+
}).then(function (pageLibRoot) {
|
|
871
|
+
modules = _inPageModules(pageLibRoot);
|
|
738
872
|
return page.evaluate(_IN_PAGE_SCENE);
|
|
739
873
|
}).then(function (pageScene) {
|
|
740
874
|
// The caller's `build`/`projection`, if any, were an EXPECTATION about what the
|
|
@@ -760,7 +894,7 @@ import { projectIdentity, projectBytes } from '../../../shim/project/identity.js
|
|
|
760
894
|
'is closed. Open a new one; a capture surface that silently ' +
|
|
761
895
|
'resurrected a browser would leak processes by design. (FAIL-LOUD.)');
|
|
762
896
|
}
|
|
763
|
-
return page.evaluate(C.CH.IN_PAGE_CAPTURE, { stage: stage, modules:
|
|
897
|
+
return page.evaluate(C.CH.IN_PAGE_CAPTURE, { stage: stage, modules: modules })
|
|
764
898
|
.then(function (env) { return C.CH.decodeStillEnvelope(env); });
|
|
765
899
|
},
|
|
766
900
|
record: function (recordOpts) {
|
|
@@ -775,14 +909,14 @@ import { projectIdentity, projectBytes } from '../../../shim/project/identity.js
|
|
|
775
909
|
var signal = recordOpts.signal || null;
|
|
776
910
|
var pageOpts = {};
|
|
777
911
|
for (var k in recordOpts) { if (k !== 'signal') { pageOpts[k] = recordOpts[k]; } }
|
|
778
|
-
var begun = page.evaluate(C.CH.IN_PAGE_RECORD_BEGIN, { id: id, opts: pageOpts, modules:
|
|
912
|
+
var begun = page.evaluate(C.CH.IN_PAGE_RECORD_BEGIN, { id: id, opts: pageOpts, modules: modules });
|
|
779
913
|
gate.opened = begun.then(null, function () {});
|
|
780
914
|
var onAbort = null;
|
|
781
915
|
var aborted = null;
|
|
782
916
|
if (signal) {
|
|
783
917
|
var sendAbort = function () {
|
|
784
918
|
return begun.then(function () {
|
|
785
|
-
return page.evaluate(C.CH.IN_PAGE_RECORD_ABORT, { id: id, modules:
|
|
919
|
+
return page.evaluate(C.CH.IN_PAGE_RECORD_ABORT, { id: id, modules: modules });
|
|
786
920
|
});
|
|
787
921
|
};
|
|
788
922
|
if (signal.aborted) { aborted = sendAbort(); }
|
|
@@ -793,7 +927,7 @@ import { projectIdentity, projectBytes } from '../../../shim/project/identity.js
|
|
|
793
927
|
}
|
|
794
928
|
var detach = function () { if (onAbort) { signal.removeEventListener('abort', onAbort); } };
|
|
795
929
|
return begun.then(function () {
|
|
796
|
-
return page.evaluate(C.CH.IN_PAGE_RECORD_END, { id: id, modules:
|
|
930
|
+
return page.evaluate(C.CH.IN_PAGE_RECORD_END, { id: id, modules: modules });
|
|
797
931
|
}).then(function (env) {
|
|
798
932
|
detach();
|
|
799
933
|
// A failed ABORT delivery is surfaced, never swallowed.
|
|
@@ -820,7 +954,7 @@ import { projectIdentity, projectBytes } from '../../../shim/project/identity.js
|
|
|
820
954
|
);
|
|
821
955
|
inst.attachCaptureHost(host);
|
|
822
956
|
|
|
823
|
-
return projectIdentity({ files: projectBytes(opts.sceneSource.files) }).then(function (sha) {
|
|
957
|
+
return projectIdentity({ files: projectBytes(linkedFiles(opts.sceneSource.files)) }).then(function (sha) {
|
|
824
958
|
inst.setSourceSha256(sha);
|
|
825
959
|
return inst.sceneRecord();
|
|
826
960
|
}).then(function (nodeScene) {
|
|
@@ -835,7 +969,7 @@ import { projectIdentity, projectBytes } from '../../../shim/project/identity.js
|
|
|
835
969
|
'them. (FAIL-LOUD — a hosted still filed under the wrong scene verifies ' +
|
|
836
970
|
'forever against the wrong header.)');
|
|
837
971
|
}
|
|
838
|
-
return _handle(browser, page, host, inst, nodeScene, gate, timeout);
|
|
972
|
+
return _handle(browser, page, host, inst, nodeScene, gate, timeout, modules);
|
|
839
973
|
});
|
|
840
974
|
}).catch(function (e) {
|
|
841
975
|
// A half-opened handle that leaked a browser would poison the zero-orphans gate
|
|
@@ -854,7 +988,7 @@ import { projectIdentity, projectBytes } from '../../../shim/project/identity.js
|
|
|
854
988
|
}
|
|
855
989
|
|
|
856
990
|
/** The consumer-facing handle. @private */
|
|
857
|
-
function _handle(browser, page, host, inst, scene, gate, timeout) {
|
|
991
|
+
function _handle(browser, page, host, inst, scene, gate, timeout, modules) {
|
|
858
992
|
var self = {
|
|
859
993
|
/**
|
|
860
994
|
* The SAME face as everywhere else: getScene / getStill / stages / toPNG, and
|
|
@@ -867,7 +1001,7 @@ import { projectIdentity, projectBytes } from '../../../shim/project/identity.js
|
|
|
867
1001
|
/** The node-side SceneRecord (page-extended) + its content id. */
|
|
868
1002
|
scene: scene,
|
|
869
1003
|
|
|
870
|
-
/** @returns {Promise<number
|
|
1004
|
+
/** @returns {Promise<number>} the world's own tick (runner.worldTick: 0 before its first step) — READ, never advanced. */
|
|
871
1005
|
tick: function () { return page.evaluate(_IN_PAGE_TICK); },
|
|
872
1006
|
|
|
873
1007
|
/**
|
|
@@ -886,7 +1020,7 @@ import { projectIdentity, projectBytes } from '../../../shim/project/identity.js
|
|
|
886
1020
|
'non-negative integer coordinate; got ' + String(n) + '. (FAIL-LOUD.)'));
|
|
887
1021
|
}
|
|
888
1022
|
return gate.opened.then(function () { return self.tick(); }).then(function (now) {
|
|
889
|
-
var at =
|
|
1023
|
+
var at = now;
|
|
890
1024
|
if (at > n) {
|
|
891
1025
|
throw new Error('[FrqtlHostedEyes.driveToTick] the hosted world is ' +
|
|
892
1026
|
'already at tick ' + at + ', past ' + n + '. A world runs forward ' +
|
|
@@ -970,6 +1104,42 @@ import { projectIdentity, projectBytes } from '../../../shim/project/identity.js
|
|
|
970
1104
|
return file;
|
|
971
1105
|
},
|
|
972
1106
|
|
|
1107
|
+
/**
|
|
1108
|
+
* The Record door, forwarded to the PAGE's facade (lib/frqtl/objects/record/Record.js): the folder is
|
|
1109
|
+
* the one the page's own door writes, so a node host and a page produce the same files for the same acts.
|
|
1110
|
+
* The handle has no re-authoring door: a Record opened by `eyes.record.start()` closes only by
|
|
1111
|
+
* `eyes.record.stop()`. A refusal carries `err.record = { changes, clips }`, as in a page.
|
|
1112
|
+
* start() exp.startRecord() → the Record's tick (0)
|
|
1113
|
+
* film(opts) rec.film(opts) → its clip spans. Drive the world while it films:
|
|
1114
|
+
* `const f = eyes.record.film(o); await eyes.driveToTick(n); await f`
|
|
1115
|
+
* stop() rec.stop() → { files: { path: string | Uint8Array }, record, clips } | { empty: true }
|
|
1116
|
+
*/
|
|
1117
|
+
record: {
|
|
1118
|
+
start: function () {
|
|
1119
|
+
return page.evaluate(_IN_PAGE_RECORD_START).then(_rethrowRefused).then(function (o) { return o.tick; });
|
|
1120
|
+
},
|
|
1121
|
+
film: function (opts) {
|
|
1122
|
+
var begun = page.evaluate(_IN_PAGE_RECORD_FILM_BEGIN, opts);
|
|
1123
|
+
gate.opened = begun.then(null, function () {});
|
|
1124
|
+
return begun.then(function () { return page.evaluate(_IN_PAGE_RECORD_FILM_END); });
|
|
1125
|
+
},
|
|
1126
|
+
stop: function () {
|
|
1127
|
+
return page.evaluate(_IN_PAGE_RECORD_STOP, { modules: modules }).then(_rethrowRefused).then(_folderFromPage);
|
|
1128
|
+
}
|
|
1129
|
+
},
|
|
1130
|
+
|
|
1131
|
+
/**
|
|
1132
|
+
* Acts through the page's own doors (the writer's rows come from these, never from node):
|
|
1133
|
+
* toSketch({ by }) exp.toSketch
|
|
1134
|
+
* setDeclared(name, patch, { by }) exp.setDeclared
|
|
1135
|
+
* placeDeclaredAt(name, [x, y, z?]) runner.placeDeclaredAt on the declared object of that name
|
|
1136
|
+
*/
|
|
1137
|
+
act: {
|
|
1138
|
+
toSketch: function (opts) { return page.evaluate(_IN_PAGE_ACT, { door: 'toSketch', opts: opts }); },
|
|
1139
|
+
setDeclared: function (name, patch, opts) { return page.evaluate(_IN_PAGE_ACT, { door: 'setDeclared', name: name, patch: patch, opts: opts }); },
|
|
1140
|
+
placeDeclaredAt: function (name, point) { return page.evaluate(_IN_PAGE_ACT, { door: 'placeDeclaredAt', name: name, point: point }); }
|
|
1141
|
+
},
|
|
1142
|
+
|
|
973
1143
|
/** Close the managed browser. Symmetric with open(), idempotent. */
|
|
974
1144
|
dispose: function () { return host.dispose(); }
|
|
975
1145
|
};
|
|
@@ -980,5 +1150,46 @@ import { projectIdentity, projectBytes } from '../../../shim/project/identity.js
|
|
|
980
1150
|
|
|
981
1151
|
var FrqtlHostedEyes = { open: open };
|
|
982
1152
|
|
|
983
|
-
|
|
1153
|
+
/**
|
|
1154
|
+
* writeRecordFolder(folder, dir) — write a Record folder (what `eyes.record.stop()` resolves) to `dir`, each path
|
|
1155
|
+
* verbatim: text as UTF-8, clip bytes as they are. The ONE node writer of the folder.
|
|
1156
|
+
* REFUSES (throws): an empty Record (`{ empty: true }` is a state with no folder); a `dir` that exists and is not
|
|
1157
|
+
* empty (a Record is never written over another); a path that is absolute or climbs out of `dir`.
|
|
1158
|
+
* @param {{ files: Object<string, string|Uint8Array> }} folder
|
|
1159
|
+
* @param {string} dir
|
|
1160
|
+
* @returns {string[]} the paths written, in the folder's order
|
|
1161
|
+
*/
|
|
1162
|
+
function writeRecordFolder(folder, dir) {
|
|
1163
|
+
if (folder && folder.empty === true) {
|
|
1164
|
+
throw new Error('[FrqtlHostedEyes] writeRecordFolder: this Record is empty — no change and no clip — a state with no folder. (FAIL-LOUD.)');
|
|
1165
|
+
}
|
|
1166
|
+
if (!folder || !folder.files || typeof folder.files !== 'object') {
|
|
1167
|
+
throw new Error('[FrqtlHostedEyes] writeRecordFolder(folder, dir): folder is what eyes.record.stop() resolves, { files, record, clips }. (FAIL-LOUD.)');
|
|
1168
|
+
}
|
|
1169
|
+
if (typeof dir !== 'string' || !dir) { throw new Error('[FrqtlHostedEyes] writeRecordFolder(folder, dir): dir is a directory path. (FAIL-LOUD.)'); }
|
|
1170
|
+
const root = path.resolve(dir);
|
|
1171
|
+
if (fs.existsSync(root) && fs.readdirSync(root).length) {
|
|
1172
|
+
throw new Error('[FrqtlHostedEyes] writeRecordFolder: ' + root + ' exists and is not empty — a Record is never written over another; ' +
|
|
1173
|
+
'name a new directory. (FAIL-LOUD.)');
|
|
1174
|
+
}
|
|
1175
|
+
const paths = Object.keys(folder.files);
|
|
1176
|
+
for (const p of paths) {
|
|
1177
|
+
const at = path.resolve(root, p);
|
|
1178
|
+
if (path.isAbsolute(p) || (at !== root && !at.startsWith(root + path.sep))) {
|
|
1179
|
+
throw new Error('[FrqtlHostedEyes] writeRecordFolder: the folder path ' + JSON.stringify(p) + ' is not inside the folder. (FAIL-LOUD.)');
|
|
1180
|
+
}
|
|
1181
|
+
const v = folder.files[p];
|
|
1182
|
+
if (typeof v !== 'string' && !(v instanceof Uint8Array)) {
|
|
1183
|
+
throw new Error('[FrqtlHostedEyes] writeRecordFolder: ' + JSON.stringify(p) + ' is neither text nor bytes. (FAIL-LOUD.)');
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
for (const p of paths) {
|
|
1187
|
+
const at = path.resolve(root, p);
|
|
1188
|
+
fs.mkdirSync(path.dirname(at), { recursive: true });
|
|
1189
|
+
fs.writeFileSync(at, folder.files[p]);
|
|
1190
|
+
}
|
|
1191
|
+
return paths;
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
export { FrqtlHostedEyes, writeRecordFolder };
|
|
984
1195
|
|
|
@@ -597,7 +597,7 @@ import { declaredCount } from '../ObjectFactory.js';
|
|
|
597
597
|
type: 'rect',
|
|
598
598
|
engineVisible: true,
|
|
599
599
|
description: 'Canonical bounding rect ' + ValueNormalizer.rectTupleText(4) + ' — repositions the single emission ' +
|
|
600
|
-
'source rect.
|
|
600
|
+
'source rect. Routed through ' +
|
|
601
601
|
'QuantumEmitter.setSourceRect() (mutates emState.sourceRects directly — the ' +
|
|
602
602
|
'RETIRED provider ABI updateEmitterSourceRects no longer exists post-G-EMIT-2). ' +
|
|
603
603
|
'The emitter moves its own marker.'
|