@descix/frqtl-node 0.0.0-stage → 0.6.0-p0.2
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 +87 -0
- package/LICENSE.md +664 -0
- package/README.md +84 -2
- package/dist/dist-manifest.json +218 -0
- package/dist/eyes/index.js +109 -0
- package/dist/fat/pkg-node/egpt_wasm.js +1277 -0
- package/dist/fat/pkg-node/egpt_wasm_bg.wasm +0 -0
- package/dist/fat/pkg-node/package.json +12 -0
- package/dist/frqtl/assets/vendor/chroma.mjs +64 -0
- package/dist/frqtl/compat/boot-node.js +34 -0
- package/dist/frqtl/compat/engine.js +127 -0
- package/dist/frqtl/compat/package-root.js +24 -0
- package/dist/frqtl/compat/providers/wasm-engine.js +2499 -0
- package/dist/frqtl/compat/providers/wasm-load-browser.js +134 -0
- package/dist/frqtl/compat/providers/wasm-load-node.js +59 -0
- package/dist/frqtl/compat/renderer/FieldBuilder.js +145 -0
- package/dist/frqtl/compat/renderer/Filters.js +163 -0
- package/dist/frqtl/compat/renderer/RenderFamily.js +84 -0
- package/dist/frqtl/compat/renderer/RenderMesh.js +99 -0
- package/dist/frqtl/compat/renderer/RenderShape.js +154 -0
- package/dist/frqtl/compat/renderer/camera2d-geometry.js +183 -0
- package/dist/frqtl/compat/renderer/captureStill-common.js +258 -0
- package/dist/frqtl/compat/renderer/display-slot.js +96 -0
- package/dist/frqtl/compat/renderer/lo-drawn-center.js +231 -0
- package/dist/frqtl/compat/renderer/orbit-bag.js +216 -0
- package/dist/frqtl/compat/renderer/providers/godrays-layer.js +213 -0
- package/dist/frqtl/compat/renderer/providers/index.js +22 -0
- package/dist/frqtl/compat/renderer/providers/p5-provider.js +861 -0
- package/dist/frqtl/compat/renderer/providers/three-camera-contract.js +160 -0
- package/dist/frqtl/compat/renderer/providers/three-camera-geometry.js +571 -0
- package/dist/frqtl/compat/renderer/providers/three-provider.js +2470 -0
- package/dist/frqtl/compat/renderer/renderCapabilities-common.js +186 -0
- package/dist/frqtl/compat/renderer/renderer-provider-contract.js +238 -0
- package/dist/frqtl/compat/renderer/unprojectStill.js +172 -0
- package/dist/frqtl/compat/vendor.js +196 -0
- package/dist/frqtl/node-entry.js +17 -0
- package/dist/frqtl/objects/FrqtlApp.js +2920 -0
- package/dist/frqtl/objects/ObjectFactory.js +296 -0
- package/dist/frqtl/objects/RecipeContext.js +322 -0
- package/dist/frqtl/objects/collectors/DataCollector.js +116 -0
- package/dist/frqtl/objects/constants/WallBehaviors.js +178 -0
- package/dist/frqtl/objects/geometry/SlabFace.js +258 -0
- package/dist/frqtl/objects/instruments/capture-host.js +453 -0
- package/dist/frqtl/objects/instruments/capture.js +710 -0
- package/dist/frqtl/objects/instruments/hosted-eyes.js +984 -0
- package/dist/frqtl/objects/kinds-entry.js +14 -0
- package/dist/frqtl/objects/kinds.js +190 -0
- package/dist/frqtl/objects/objects/Box.js +273 -0
- package/dist/frqtl/objects/objects/Bullet.js +155 -0
- package/dist/frqtl/objects/objects/CircuitGate.js +380 -0
- package/dist/frqtl/objects/objects/CircuitProbe.js +214 -0
- package/dist/frqtl/objects/objects/DirectedTunnel.js +411 -0
- package/dist/frqtl/objects/objects/EgptFractal.js +164 -0
- package/dist/frqtl/objects/objects/LightSource.js +351 -0
- package/dist/frqtl/objects/objects/QuantumEmitter.js +760 -0
- package/dist/frqtl/objects/objects/Tube.js +471 -0
- package/dist/frqtl/objects/objects/absorbRadiation.js +309 -0
- package/dist/frqtl/objects/objects/actionCompiler.js +1747 -0
- package/dist/frqtl/objects/objects/controlAction.js +89 -0
- package/dist/frqtl/objects/objects/controlBind.js +278 -0
- package/dist/frqtl/objects/objects/directedRedirectProgram.js +120 -0
- package/dist/frqtl/objects/objects/emitterProgram.js +209 -0
- package/dist/frqtl/objects/objects/frqtl-control-slider.js +197 -0
- package/dist/frqtl/objects/objects/frqtl-control-sync.js +272 -0
- package/dist/frqtl/objects/objects/frqtl-control-widgets.js +219 -0
- package/dist/frqtl/objects/objects/frqtl-control-write-queue.js +174 -0
- package/dist/frqtl/objects/objects/makeControlsForObject.js +820 -0
- package/dist/frqtl/objects/objects/makeControlsForRenderSurface.js +161 -0
- package/dist/frqtl/objects/objects/slitLinerScatter.js +261 -0
- package/dist/frqtl/objects/objects/sustainedSource.js +298 -0
- package/dist/frqtl/objects/objects/tagger.js +228 -0
- package/dist/frqtl/objects/schema/engine-scopes.js +468 -0
- package/dist/frqtl/objects/schema/index.js +256 -0
- package/dist/frqtl/objects/schema/scopes.js +457 -0
- package/dist/frqtl/objects/schema/validate-descriptor.js +49 -0
- package/dist/frqtl/objects/schema/valueNormalizer.js +366 -0
- package/dist/frqtl/objects/setup/_collisionMatrixLanes.js +66 -0
- package/dist/frqtl/objects/setup/_setupOptions.js +118 -0
- package/dist/frqtl/objects/setup/_worldInit.js +162 -0
- package/dist/frqtl/objects/setup/setupBlackbodySubstrate.js +242 -0
- package/dist/frqtl/objects/setup/setupCircuitBasic.js +447 -0
- package/dist/frqtl/objects/setup/setupCollisionMatrix.js +188 -0
- package/dist/frqtl/objects/setup/setupConeEmitter3D.js +250 -0
- package/dist/frqtl/objects/setup/setupDoubleSlit.js +326 -0
- package/dist/frqtl/objects/setup/setupDoubleSlit_Laser.js +229 -0
- package/dist/frqtl/objects/setup/setupEmpty.js +59 -0
- package/dist/frqtl/objects/setup/setupFxSmoke.js +209 -0
- package/dist/frqtl/objects/setup/setupG1Race.js +80 -0
- package/dist/frqtl/objects/setup/setupG2Sweep.js +89 -0
- package/dist/frqtl/objects/setup/setupG3Collide.js +177 -0
- package/dist/frqtl/objects/setup/setupLightSourceLab.js +277 -0
- package/dist/frqtl/objects/setup/setupOscillator.js +68 -0
- package/dist/frqtl/objects/setup/setupParticleWalk.js +93 -0
- package/dist/frqtl/objects/setup/setupParticleWalk3D.js +133 -0
- package/dist/frqtl/objects/setup/setupSphereEmitter3D.js +1258 -0
- package/dist/frqtl/objects/setup/setupTraceCircuit.js +239 -0
- package/dist/frqtl/objects/setup/setupTunnelLab.js +240 -0
- package/dist/frqtl/objects/setup/setupTunnelPointExit.js +306 -0
- package/dist/frqtl/objects/setup/setupWaveInterference.js +177 -0
- package/dist/frqtl/objects/v1-replacements.js +29 -0
- package/dist/frqtl/package-core.js +23 -0
- package/dist/frqtl/parts/CameraControls.js +643 -0
- package/dist/frqtl/parts/CameraPath.js +575 -0
- package/dist/frqtl/parts/CollisionActions.js +34 -0
- package/dist/frqtl/parts/CollisionTypes.js +30 -0
- package/dist/frqtl/parts/DetectorWall.js +496 -0
- package/dist/frqtl/parts/FrqtlControlOverlay.js +227 -0
- package/dist/frqtl/parts/FrqtlControlsPanel.js +415 -0
- package/dist/frqtl/parts/FrqtlVision.js +818 -0
- package/dist/frqtl/parts/GraphingDetectorWall.js +489 -0
- package/dist/frqtl/parts/LargeObject.js +405 -0
- package/dist/frqtl/parts/LightColor.js +255 -0
- package/dist/frqtl/parts/LiveChart.js +285 -0
- package/dist/frqtl/parts/LogicGate.js +717 -0
- package/dist/frqtl/parts/OvenBox.js +611 -0
- package/dist/frqtl/parts/PointSource.js +762 -0
- package/dist/frqtl/parts/Rectangle.js +200 -0
- package/dist/frqtl/parts/SlitLightSource.js +234 -0
- package/dist/frqtl/parts/SlitScreen.js +628 -0
- package/dist/frqtl/parts/SphereEmitter.js +946 -0
- package/dist/frqtl/parts/Trace.js +508 -0
- package/dist/frqtl/parts/Tunnel.js +1057 -0
- package/dist/frqtl/parts/Wall.js +1211 -0
- package/dist/frqtl/parts/index.js +33 -0
- package/dist/frqtl/scenes/absorbing-wall-3d.js +33 -0
- package/dist/frqtl/scenes/absorbing-wall.js +32 -0
- package/dist/frqtl/scenes/atomic-model.js +88 -0
- package/dist/frqtl/scenes/big-bang.js +46 -0
- package/dist/frqtl/scenes/blackbody.js +81 -0
- package/dist/frqtl/scenes/circuit-sat.js +127 -0
- package/dist/frqtl/scenes/collision-matrix-3d.js +107 -0
- package/dist/frqtl/scenes/counting-wall-3d.js +36 -0
- package/dist/frqtl/scenes/counting-wall.js +38 -0
- package/dist/frqtl/scenes/double-slit.js +31 -0
- package/dist/frqtl/scenes/dse-one-emitter.js +196 -0
- package/dist/frqtl/scenes/dse-one-slit.js +114 -0
- package/dist/frqtl/scenes/dse-photoelectric.js +70 -0
- package/dist/frqtl/scenes/dse-tunnel-rebuild.js +243 -0
- package/dist/frqtl/scenes/dse-two-emitters.js +196 -0
- package/dist/frqtl/scenes/dse-two-slits.js +132 -0
- package/dist/frqtl/scenes/empty.js +14 -0
- package/dist/frqtl/scenes/laser-buildup.js +49 -0
- package/dist/frqtl/scenes/mirror-3d.js +56 -0
- package/dist/frqtl/scenes/mirror-route.js +140 -0
- package/dist/frqtl/scenes/mirror.js +38 -0
- package/dist/frqtl/scenes/omni-3d.js +27 -0
- package/dist/frqtl/scenes/omni-source.js +22 -0
- package/dist/frqtl/scenes/self-interference.js +39 -0
- package/dist/frqtl/scenes/smoking-gun.js +122 -0
- package/dist/frqtl/scenes/straight-walk.js +22 -0
- package/dist/frqtl/scenes/sun.js +39 -0
- package/dist/frqtl/scenes/two-source.js +37 -0
- package/dist/frqtl/scenes/wave.js +26 -0
- package/dist/shim/bundle/frqnb.js +112 -0
- package/dist/shim/bundle/index.js +121 -0
- package/dist/shim/bundle/indexHtml.js +107 -0
- package/dist/shim/bundle/manifest.js +95 -0
- package/dist/shim/bundle/zip.js +243 -0
- package/dist/shim/bus.js +67 -0
- package/dist/shim/controls/index.js +627 -0
- package/dist/shim/controls/registry.js +274 -0
- package/dist/shim/engine/artifact-shell.js +211 -0
- package/dist/shim/engine/boot-phases.js +44 -0
- package/dist/shim/engine/engine-stack-boot.js +59 -0
- package/dist/shim/engine/router.js +690 -0
- package/dist/shim/experiment-frame/index.js +296 -0
- package/dist/shim/expr/ast-schema.js +369 -0
- package/dist/shim/field-descriptor/index.js +325 -0
- package/dist/shim/frame-data/index.js +224 -0
- package/dist/shim/index.js +67 -0
- package/dist/shim/notebook/canvas-guard.js +82 -0
- package/dist/shim/notebook/catalog.js +35 -0
- package/dist/shim/notebook/format.js +736 -0
- package/dist/shim/notebook/frqtl-setups-importmap.js +81 -0
- package/dist/shim/notebook/kernel.js +671 -0
- package/dist/shim/notebook/knob.js +150 -0
- package/dist/shim/notebook/module-resolver.js +260 -0
- package/dist/shim/notebook/notebook-knobs.js +65 -0
- package/dist/shim/notebook/produced-bindings.js +67 -0
- package/dist/shim/notebook/realm-registry.js +121 -0
- package/dist/shim/notebook/realm-transport.js +73 -0
- package/dist/shim/notebook/setup-builder.js +507 -0
- package/dist/shim/notebook/store.js +159 -0
- package/dist/shim/notebook/vendor-importmap.js +134 -0
- package/dist/shim/notebook/view-builder.js +189 -0
- package/dist/shim/notebook/view-ctx.js +100 -0
- package/dist/shim/notebook/view-handle.js +202 -0
- package/dist/shim/notebook/warmup.js +102 -0
- package/dist/shim/package.json +4 -0
- package/dist/shim/page-params/index.js +213 -0
- package/dist/shim/project/identity.js +112 -0
- package/dist/shim/project/index.js +19 -0
- package/dist/shim/project/link.js +402 -0
- package/dist/shim/project/node-specifier-hook.mjs +35 -0
- package/dist/shim/project/register-node.mjs +17 -0
- package/dist/shim/project/specifiers.js +106 -0
- package/dist/shim/registry/copy-gate.js +110 -0
- package/dist/shim/registry/exhibits.js +455 -0
- package/dist/shim/registry/experiments.js +215 -0
- package/dist/shim/registry/index.js +19 -0
- package/dist/shim/registry/scene-source.js +157 -0
- package/dist/shim/registry/walkthrough.js +804 -0
- package/dist/shim/render/capture-envelope.js +279 -0
- package/dist/shim/render/heat-colormap.js +92 -0
- package/dist/shim/render/render-stages.js +855 -0
- package/dist/shim/render/sun-look.js +33 -0
- package/dist/shim/render-state/apply.js +262 -0
- package/dist/shim/render-state/index.js +74 -0
- package/dist/shim/rpc/index.js +207 -0
- package/dist/shim/runtime/index.js +1023 -0
- package/dist/shim/storage/boot.js +69 -0
- package/dist/shim/storage/index.js +169 -0
- package/dist/shim/storage/local-storage-provider.js +230 -0
- package/dist/shim/storage/make-namespace-store.js +132 -0
- package/dist/shim/storage/modules.js +249 -0
- package/dist/shim/storage/projects.js +150 -0
- package/llms.txt +12 -0
- package/package.json +63 -4
|
@@ -0,0 +1,453 @@
|
|
|
1
|
+
/* Copyright 2023-2026 by Essam Abadir */
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* capture-host.js — THE CAPTURE-HOST KIND: a stage executor that runs REMOTELY.
|
|
5
|
+
*
|
|
6
|
+
* Peer of `capture.js` in this directory (the tap MECHANISM) and deliberately narrower:
|
|
7
|
+
* `capture.js` owns dispatch, throw rules and provenance for stages this process can
|
|
8
|
+
* execute; a CAPTURE HOST executes a stage in ANOTHER process — a managed headless
|
|
9
|
+
* browser running the real render stack — and hands back a bag that was already
|
|
10
|
+
* normalized THERE. This file is the contract that makes that safe: the KIND's shape,
|
|
11
|
+
* the ONE boundary codec, and the verification that proves the two processes are
|
|
12
|
+
* talking about the SAME scene.
|
|
13
|
+
*
|
|
14
|
+
* Contract: RENDERER_CONTRACT §9 (the capture-host KIND) and
|
|
15
|
+
* §10.3 (the normalized format this ferries).
|
|
16
|
+
*
|
|
17
|
+
* ── Why a distinct KIND and not a provider-shaped adapter (V3) ──────────────────
|
|
18
|
+
*
|
|
19
|
+
* A `RendererProvider` adapter would have been the tempting shape: implement
|
|
20
|
+
* `captureStill()` in node, forward to the browser, let the local dispatcher stamp the
|
|
21
|
+
* provenance. It is WRONG, and not by taste — the local process has none of the facts
|
|
22
|
+
* the provenance is made of. It has no tick (the world is in the page), no pose (the
|
|
23
|
+
* camera is in the page), no ordered filter list, no render options, no canvas dims. A
|
|
24
|
+
* provider-shaped adapter would therefore have had to INVENT them, which is the
|
|
25
|
+
* caller-supplied-provenance anti-pattern at industrial scale: a corpus row that looks
|
|
26
|
+
* fully stamped and is fiction.
|
|
27
|
+
*
|
|
28
|
+
* So the KIND inverts the arrow. **The IN-PAGE dispatcher stamps everything** (it is a
|
|
29
|
+
* real `FrqtlCapture` over a real loop and a real provider — the ONE authority, the same
|
|
30
|
+
* code the browser user runs), and the node side's job is not to stamp but to VERIFY:
|
|
31
|
+
* every still it hands back must carry the SAME scene identity the node side computed
|
|
32
|
+
* independently. Measured fidelity, not trust.
|
|
33
|
+
*
|
|
34
|
+
* ── The KIND (what an implementation must be) ─────────────────────────────────
|
|
35
|
+
*
|
|
36
|
+
* {
|
|
37
|
+
* kind: 'capture-host', // the KIND tag — asserted, never inferred
|
|
38
|
+
* stages: string[], // stages this executor executes REMOTELY
|
|
39
|
+
* identity(): { build, projection }, // SYNC — resolved once at open()
|
|
40
|
+
* capture(stage): Promise<Still>, // a FULLY NORMALIZED bag, stamped remotely
|
|
41
|
+
* record(opts): Promise<VideoEnvelope>, // FrqtlVision.record, run where the world is
|
|
42
|
+
* // (opts.signal aborts it across the boundary)
|
|
43
|
+
* dispose(): Promise<void> // symmetric with whatever open() acquired
|
|
44
|
+
* }
|
|
45
|
+
*
|
|
46
|
+
* `record` FORWARDS to the one recorder, `FrqtlVision.record`, running in the host's page
|
|
47
|
+
* over its own runner — the tick listener, the hold, the stills and the encoder are all
|
|
48
|
+
* there. The host adds no recording logic; the node side re-validates the returned envelope
|
|
49
|
+
* against its owner and VERIFIES its scene (`verifyVideo`), exactly as it verifies a still.
|
|
50
|
+
*
|
|
51
|
+
* `identity()` is SYNCHRONOUS on purpose: `sceneRecord()` builds its record from live
|
|
52
|
+
* field reads and only its HASH is async, so an async identity read here would have
|
|
53
|
+
* forced the record build itself to become async and rippled through every caller. An
|
|
54
|
+
* implementation resolves the page's `build`/`projection` during its own async `open()`
|
|
55
|
+
* and serves them from a field afterwards — they cannot change within a run anyway
|
|
56
|
+
* (§10.3: the display provider is boot-fixed).
|
|
57
|
+
*
|
|
58
|
+
* ── a.3 · identity convergence (the finding that would have split every corpus row) ──
|
|
59
|
+
*
|
|
60
|
+
* As-built, a node-side identity carries `build: {engine:null, renderer:null, …}` and
|
|
61
|
+
* `projection: null`, because those are derived from BROWSER boot authorities that do not
|
|
62
|
+
* exist in node. The page's identity carries the real ones. Two different records ⇒ two
|
|
63
|
+
* different hashes ⇒ EVERY hosted row would have declared a different scene than the
|
|
64
|
+
* browser row of the same scene, silently, forever.
|
|
65
|
+
*
|
|
66
|
+
* The fix is structural, not a comparison relaxation: attaching a host EXTENDS the node
|
|
67
|
+
* identity with the page-resolved `build`/`projection` (`capture.js` reads them through
|
|
68
|
+
* the attached host), so the two records are equal BY CONSTRUCTION and the hash check is
|
|
69
|
+
* a real check rather than a tautology waiting to be loosened. What remains genuinely
|
|
70
|
+
* node-owned — `seed`, `sourceRef` — is what the host must make the page agree to (it boots
|
|
71
|
+
* with `?seed=` and registers the source ref), and `diffSceneRecords` below names any survivor
|
|
72
|
+
* by field. The physics bounds are a boot RESULT: the scene's co-init call writes them, the
|
|
73
|
+
* host reads them back from the page, and a caller's stated bounds are an EXPECTATION.
|
|
74
|
+
*
|
|
75
|
+
* ── Layer 2: physics behaviour ───────────────────────────────────────────────
|
|
76
|
+
* ZERO physics. This is the output boundary, in two processes instead of one. Nothing
|
|
77
|
+
* here touches a frame, a dimension, a tick or an op-program, and the node side never
|
|
78
|
+
* drives a clock: the page's own loop is the tick authority (a second clock across a
|
|
79
|
+
* process boundary is the two-clocks bug with network latency added).
|
|
80
|
+
*/
|
|
81
|
+
|
|
82
|
+
import * as RenderStages from '../../../shim/render/render-stages.js';
|
|
83
|
+
import * as CaptureEnvelope from '../../../shim/render/capture-envelope.js';
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
/** The KIND tag. A `RendererProvider` does not carry it — that is the point. */
|
|
87
|
+
var CAPTURE_HOST_KIND = 'capture-host';
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* assertCaptureHost(host) — throw-or-return the KIND gate.
|
|
91
|
+
*
|
|
92
|
+
* The `kind` tag is checked FIRST and by exact string, so the most likely mistake —
|
|
93
|
+
* handing this a provider-shaped object because "it has captureStill()" — names its
|
|
94
|
+
* own diagnosis instead of failing later on a missing method.
|
|
95
|
+
*
|
|
96
|
+
* @param {object} host
|
|
97
|
+
* @returns {object} the same host, when valid
|
|
98
|
+
* @throws {Error} FAIL-LOUD on any missing/mis-shaped member.
|
|
99
|
+
*/
|
|
100
|
+
function assertCaptureHost(host) {
|
|
101
|
+
var RS = RenderStages;
|
|
102
|
+
if (!host || typeof host !== 'object') {
|
|
103
|
+
throw new Error('[FrqtlCaptureHost] expected a capture-host object, got ' +
|
|
104
|
+
String(host) + '. (FAIL-LOUD.)');
|
|
105
|
+
}
|
|
106
|
+
if (host.kind !== CAPTURE_HOST_KIND) {
|
|
107
|
+
throw new Error('[FrqtlCaptureHost] this object is not a ' +
|
|
108
|
+
JSON.stringify(CAPTURE_HOST_KIND) + ' (its kind is ' + JSON.stringify(host.kind) +
|
|
109
|
+
'). A capture host is a DISTINCT stage-executor kind, not a RendererProvider: a ' +
|
|
110
|
+
'provider returns raw pixels and lets the local dispatcher stamp provenance, ' +
|
|
111
|
+
'which a remote executor cannot honestly do — it holds no tick, no pose, no ' +
|
|
112
|
+
'filter order. A host returns a bag already normalized by the dispatcher that ' +
|
|
113
|
+
'DOES hold them. (FAIL-LOUD.)');
|
|
114
|
+
}
|
|
115
|
+
if (!Array.isArray(host.stages) || host.stages.length === 0) {
|
|
116
|
+
throw new Error('[FrqtlCaptureHost] `stages` must be a non-empty array of stage ' +
|
|
117
|
+
'names this host executes remotely — [' + RS.RENDER_STAGES.join(', ') +
|
|
118
|
+
']. (FAIL-LOUD.)');
|
|
119
|
+
}
|
|
120
|
+
for (var i = 0; i < host.stages.length; i++) { RS.assertStage(host.stages[i]); }
|
|
121
|
+
['identity', 'capture', 'record', 'dispose'].forEach(function (m) {
|
|
122
|
+
if (typeof host[m] !== 'function') {
|
|
123
|
+
throw new Error('[FrqtlCaptureHost] a capture host must implement ' + m + '() — ' +
|
|
124
|
+
'the KIND is {kind, stages, identity(), capture(stage), record(opts), dispose()}. ' +
|
|
125
|
+
'(FAIL-LOUD.)');
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
var ident = host.identity();
|
|
129
|
+
if (!ident || typeof ident !== 'object') {
|
|
130
|
+
throw new Error('[FrqtlCaptureHost] identity() must return the page-resolved ' +
|
|
131
|
+
'{build, projection} bag SYNCHRONOUSLY (resolved once during the host\'s own ' +
|
|
132
|
+
'open()); got ' + String(ident) + '. Without it the node-side scene identity ' +
|
|
133
|
+
'carries nulls where the page carries real values, and every hosted row splits ' +
|
|
134
|
+
'into a second scene. (FAIL-LOUD.)');
|
|
135
|
+
}
|
|
136
|
+
return host;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// ── The BOUNDARY CODEC — authored ONCE, here ─────────────────────────────
|
|
140
|
+
//
|
|
141
|
+
// Exactly one thing crosses the process boundary: the normalized bag. Never a raw
|
|
142
|
+
// pixel buffer plus reassembly code on the node side, which would mint a second
|
|
143
|
+
// encoder beside `FrqtlVision.toDataURL` and let the two disagree about row order,
|
|
144
|
+
// colour space or alpha.
|
|
145
|
+
//
|
|
146
|
+
// The problem the codec solves is narrow and real: a browser-automation `evaluate`
|
|
147
|
+
// boundary is JSON-shaped, and a typed array does not survive it (it arrives as an
|
|
148
|
+
// object with integer keys — silently, and 4x the bytes). So the payload rides as
|
|
149
|
+
// base64 and is rebuilt into the SAME typed-array constructor the page produced.
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* IN_PAGE_CAPTURE — THE IN-PAGE HALF OF THE CODEC. **This function runs in the PAGE.**
|
|
153
|
+
*
|
|
154
|
+
* It is handed to a browser `evaluate()` as source text, so it closes over NOTHING: the
|
|
155
|
+
* runner is the page door's (`window.FrqtlPage.runner`), and every module it needs is
|
|
156
|
+
* IMPORTED in the page from the URL the host hands it (`args.modules`, computed by the
|
|
157
|
+
* hosted eyes from the page URL — the page's own instances, never a copy). Keep it
|
|
158
|
+
* self-contained — a captured variable here fails at runtime in the page, not at build
|
|
159
|
+
* time here.
|
|
160
|
+
*
|
|
161
|
+
* It performs NO capture logic of its own: it calls the page's `capture(stage)` — the
|
|
162
|
+
* one dispatcher, which stamps all provenance — and then encodes the result for
|
|
163
|
+
* transport. Encoding is declared IN the envelope (`encoding`), so the node half
|
|
164
|
+
* obeys the page's declaration instead of re-deriving the rule.
|
|
165
|
+
*
|
|
166
|
+
* @param {{stage: string, modules: {captureHost: string, captureEnvelope: string}}} args
|
|
167
|
+
* @returns {Promise<{bag: object, encoding: string, ctor: (string|null), payload: *}>}
|
|
168
|
+
*/
|
|
169
|
+
var IN_PAGE_CAPTURE = async function (args) {
|
|
170
|
+
var app = window.FrqtlPage && window.FrqtlPage.runner;
|
|
171
|
+
if (!app) {
|
|
172
|
+
throw new Error('[FrqtlCaptureHost/in-page] no FrqtlPage.runner on this page — the ' +
|
|
173
|
+
'host booted something that is not a running FRQTL stack. (FAIL-LOUD.)');
|
|
174
|
+
}
|
|
175
|
+
if (!args || !args.modules || typeof args.modules.captureEnvelope !== 'string') {
|
|
176
|
+
throw new Error('[FrqtlCaptureHost/in-page] no CaptureEnvelope module URL (args.modules.captureEnvelope) — ' +
|
|
177
|
+
'the ONE transport encoder (lib/shim/render/capture-envelope.js) is imported from the page, never ' +
|
|
178
|
+
're-encoded by a copy. (FAIL-LOUD.)');
|
|
179
|
+
}
|
|
180
|
+
var CE = await import(args.modules.captureEnvelope);
|
|
181
|
+
var still = await app.capture(args.stage);
|
|
182
|
+
var data = still.data;
|
|
183
|
+
var bag = {};
|
|
184
|
+
for (var k in still) { if (k !== 'data') { bag[k] = still[k]; } }
|
|
185
|
+
// A typed array does not survive a JSON boundary; a plain framedata array does
|
|
186
|
+
// (the normalizer already flattened it to plain data — that is what makes the
|
|
187
|
+
// whole format serialization-safe). The choice is declared, never re-derived.
|
|
188
|
+
if (data && typeof data.BYTES_PER_ELEMENT === 'number') {
|
|
189
|
+
// The ONE transportable-bytes encoder (lib/shim/render/capture-envelope.js) — never a copy here.
|
|
190
|
+
return { bag: bag, encoding: 'base64', ctor: data.constructor.name,
|
|
191
|
+
payload: CE.bytesToBase64(data) };
|
|
192
|
+
}
|
|
193
|
+
return { bag: bag, encoding: 'plain', ctor: null, payload: data };
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* IN_PAGE_RECORD_BEGIN / _END / _ABORT — the recorder, in the PAGE, in three calls.
|
|
198
|
+
* **All run in the PAGE** and close over nothing.
|
|
199
|
+
*
|
|
200
|
+
* BEGIN imports the page's capture-host instance (`args.modules.captureHost`) and calls the
|
|
201
|
+
* ONE recorder it holds — `FrqtlVision`, which registers itself with capture-host when its
|
|
202
|
+
* module evaluates (`registerRecorder`) — as `recorder().attach(FrqtlPage.runner).record(opts)`.
|
|
203
|
+
* That subscribes to the tick fan-out SYNCHRONOUSLY; BEGIN parks its Promise plus an
|
|
204
|
+
* `AbortController` (whose signal the page-side record is given — a node-side signal cannot
|
|
205
|
+
* cross `evaluate`) under `id` in the page instance's ledger (`parkRecording`) — so a drive the
|
|
206
|
+
* node side issues after BEGIN resolves cannot run ticks the recorder did not see. END awaits
|
|
207
|
+
* the parked Promise and removes the entry once it settles. ABORT aborts the parked controller
|
|
208
|
+
* and returns true, or returns false when no recording is parked under `id` (it already
|
|
209
|
+
* settled — aborting a finished operation is not an error in the AbortSignal contract). The
|
|
210
|
+
* envelope crosses the boundary as plain JSON: its bytes are already base64 by the envelope
|
|
211
|
+
* owner's contract.
|
|
212
|
+
*
|
|
213
|
+
* @param {{id: number, opts: object, modules: {captureHost: string}}} args (END and ABORT take {id, modules})
|
|
214
|
+
*/
|
|
215
|
+
var IN_PAGE_RECORD_BEGIN = async function (args) {
|
|
216
|
+
var app = window.FrqtlPage && window.FrqtlPage.runner;
|
|
217
|
+
if (!app) {
|
|
218
|
+
throw new Error('[FrqtlCaptureHost/in-page] no FrqtlPage.runner on this page — the ' +
|
|
219
|
+
'host booted something that is not a running FRQTL stack. (FAIL-LOUD.)');
|
|
220
|
+
}
|
|
221
|
+
var CH = (await import(args.modules.captureHost)).FrqtlCaptureHost;
|
|
222
|
+
var controller = new AbortController();
|
|
223
|
+
var opts = {};
|
|
224
|
+
for (var k in args.opts) { opts[k] = args.opts[k]; }
|
|
225
|
+
opts.signal = controller.signal;
|
|
226
|
+
var pending = CH.recorder().attach(app).record(opts);
|
|
227
|
+
pending.then(null, function () {}); // delivered by END, never unhandled meanwhile
|
|
228
|
+
CH.parkRecording(args.id, { pending: pending, controller: controller });
|
|
229
|
+
return args.id;
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
var IN_PAGE_RECORD_END = async function (args) {
|
|
233
|
+
var CH = (await import(args.modules.captureHost)).FrqtlCaptureHost;
|
|
234
|
+
var entry = CH.parkedRecording(args.id);
|
|
235
|
+
if (!entry) {
|
|
236
|
+
throw new Error('[FrqtlCaptureHost/in-page] no recording parked under id ' + args.id +
|
|
237
|
+
'. (FAIL-LOUD.)');
|
|
238
|
+
}
|
|
239
|
+
var forget = function () { CH.forgetRecording(args.id); };
|
|
240
|
+
return entry.pending.then(function (env) { forget(); return env; },
|
|
241
|
+
function (e) { forget(); throw e; });
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
var IN_PAGE_RECORD_ABORT = async function (args) {
|
|
245
|
+
var CH = (await import(args.modules.captureHost)).FrqtlCaptureHost;
|
|
246
|
+
var entry = CH.parkedRecording(args.id);
|
|
247
|
+
if (!entry) { return false; }
|
|
248
|
+
entry.controller.abort();
|
|
249
|
+
return true;
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
// ── The page instance's state: the ONE recorder, and the hosted-recording ledger ──
|
|
253
|
+
|
|
254
|
+
var _recorder = null;
|
|
255
|
+
/** FrqtlVision registers itself here when its module evaluates — capture-host never names it. */
|
|
256
|
+
function registerRecorder(recorder) {
|
|
257
|
+
if (!recorder || typeof recorder.attach !== 'function') {
|
|
258
|
+
throw new TypeError('[FrqtlCaptureHost] registerRecorder(recorder): expected the FrqtlVision face ' +
|
|
259
|
+
'(attach(runner) → handle). (FAIL-LOUD.)');
|
|
260
|
+
}
|
|
261
|
+
if (_recorder && _recorder !== recorder) {
|
|
262
|
+
throw new Error('[FrqtlCaptureHost] a second recorder registered in this realm — FrqtlVision ' +
|
|
263
|
+
'loaded under two module URLs (one module, one URL). (FAIL-LOUD.)');
|
|
264
|
+
}
|
|
265
|
+
_recorder = recorder;
|
|
266
|
+
}
|
|
267
|
+
/** @returns the registered recorder. THROWS when none registered (FrqtlVision never imported here). */
|
|
268
|
+
function recorder() {
|
|
269
|
+
if (!_recorder) {
|
|
270
|
+
throw new Error('[FrqtlCaptureHost] no recorder registered in this realm — import FrqtlVision ' +
|
|
271
|
+
"(import { FrqtlVision } from '@descix/frqtl-web/parts'); the boot imports the parts barrel. (FAIL-LOUD.)");
|
|
272
|
+
}
|
|
273
|
+
return _recorder;
|
|
274
|
+
}
|
|
275
|
+
var _parked = Object.create(null);
|
|
276
|
+
function parkRecording(id, entry) { _parked[id] = entry; }
|
|
277
|
+
function parkedRecording(id) { return _parked[id] || null; }
|
|
278
|
+
function forgetRecording(id) { delete _parked[id]; }
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* decodeStillEnvelope(env) — THE NODE-SIDE HALF of the codec.
|
|
282
|
+
*
|
|
283
|
+
* Rebuilds the payload and re-runs the OWNER'S normalizer over the bag. Re-normalizing
|
|
284
|
+
* is not a second stamping: every provenance field is the page's, ferried verbatim —
|
|
285
|
+
* what the normalizer adds is VALIDATION (a payload that arrived empty, a format that
|
|
286
|
+
* does not match its stage, a missing scene id all fail loud here rather than entering
|
|
287
|
+
* a corpus) and the plain-data snapshot the contract promises.
|
|
288
|
+
*
|
|
289
|
+
* The payload's TYPE is ferried (`ctor`), not mapped: a local format→constructor table
|
|
290
|
+
* would be a hand mirror of the stage table's own declaration, and the page already
|
|
291
|
+
* knows the answer exactly.
|
|
292
|
+
*
|
|
293
|
+
* @param {object} env — an `IN_PAGE_CAPTURE` envelope
|
|
294
|
+
* @returns {object} a full normalized still bag
|
|
295
|
+
*/
|
|
296
|
+
function decodeStillEnvelope(env) {
|
|
297
|
+
var RS = RenderStages;
|
|
298
|
+
if (!env || typeof env !== 'object' || !env.bag) {
|
|
299
|
+
throw new Error('[FrqtlCaptureHost.decodeStillEnvelope] expected a capture envelope ' +
|
|
300
|
+
'{bag, encoding, ctor, payload}, got ' + String(env) + '. (FAIL-LOUD.)');
|
|
301
|
+
}
|
|
302
|
+
var data;
|
|
303
|
+
if (env.encoding === 'base64') {
|
|
304
|
+
if (typeof Buffer === 'undefined' || typeof Buffer.from !== 'function') {
|
|
305
|
+
throw new Error('[FrqtlCaptureHost.decodeStillEnvelope] base64 payloads are ' +
|
|
306
|
+
'decoded with Node\'s Buffer — the node half of the boundary runs in Node by ' +
|
|
307
|
+
'definition. (FAIL-LOUD.)');
|
|
308
|
+
}
|
|
309
|
+
var Ctor = (typeof globalThis !== 'undefined') ? globalThis[env.ctor] : null;
|
|
310
|
+
if (typeof Ctor !== 'function' || typeof Ctor.BYTES_PER_ELEMENT !== 'number') {
|
|
311
|
+
throw new Error('[FrqtlCaptureHost.decodeStillEnvelope] the envelope declares ' +
|
|
312
|
+
'payload type ' + JSON.stringify(env.ctor) + ', which is not a typed-array ' +
|
|
313
|
+
'constructor here. The payload TYPE is ferried from the page, never mapped ' +
|
|
314
|
+
'from the format — a local table would be a hand mirror. (FAIL-LOUD.)');
|
|
315
|
+
}
|
|
316
|
+
data = new Ctor(Buffer.from(env.payload, 'base64'));
|
|
317
|
+
} else if (env.encoding === 'plain') {
|
|
318
|
+
data = env.payload;
|
|
319
|
+
} else {
|
|
320
|
+
throw new Error('[FrqtlCaptureHost.decodeStillEnvelope] unknown payload encoding ' +
|
|
321
|
+
JSON.stringify(env.encoding) + ' — the ENCODING SIDE declares it (base64 for a ' +
|
|
322
|
+
'typed buffer, plain for JSON-safe data) and this side obeys. (FAIL-LOUD.)');
|
|
323
|
+
}
|
|
324
|
+
var partial = {};
|
|
325
|
+
for (var k in env.bag) { partial[k] = env.bag[k]; }
|
|
326
|
+
partial.data = data;
|
|
327
|
+
return RS.normalizeStill(partial);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// ── VERIFICATION — the node side proves, per still, that it is the same scene ──
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* diffSceneRecords(a, b) — the `SCENE_KEYS` on which two records disagree.
|
|
334
|
+
*
|
|
335
|
+
* Driven off the OWNER'S key list and its OWN canonical encoding, so a scene field
|
|
336
|
+
* added to the contract is compared here with no edit. Used at attach time to turn
|
|
337
|
+
* "the hashes differ" — the least actionable error a content-addressed format can
|
|
338
|
+
* produce — into "seed, physicsH".
|
|
339
|
+
*
|
|
340
|
+
* @param {object} a
|
|
341
|
+
* @param {object} b
|
|
342
|
+
* @returns {string[]} the differing key names, in table order
|
|
343
|
+
*/
|
|
344
|
+
function diffSceneRecords(a, b) {
|
|
345
|
+
var RS = RenderStages;
|
|
346
|
+
var out = [];
|
|
347
|
+
for (var i = 0; i < RS.SCENE_KEYS.length; i++) {
|
|
348
|
+
var k = RS.SCENE_KEYS[i];
|
|
349
|
+
// canonicalizeScene is the ONE canonical encoding; projecting a single field
|
|
350
|
+
// through a one-key record reuses it rather than re-implementing key-sorted
|
|
351
|
+
// deep equality here.
|
|
352
|
+
var ea = RS.canonicalizeScene(_one(k, a));
|
|
353
|
+
var eb = RS.canonicalizeScene(_one(k, b));
|
|
354
|
+
if (ea !== eb) { out.push(k); }
|
|
355
|
+
}
|
|
356
|
+
return out;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/** A record carrying ONE key's value, for the per-field canonical compare. @private */
|
|
360
|
+
function _one(key, rec) {
|
|
361
|
+
var r = {};
|
|
362
|
+
r[key] = rec ? rec[key] : undefined;
|
|
363
|
+
return r;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* verifyStill(expectedSceneId, stage, still) — the per-still fidelity check.
|
|
368
|
+
*
|
|
369
|
+
* The node side computed a scene identity independently; the page stamped one into the
|
|
370
|
+
* still. Equal ⇒ the two processes agree about what was captured. Unequal ⇒ something
|
|
371
|
+
* moved mid-run (a re-init, a resize, a re-seeded reload) and the row is REFUSED — a
|
|
372
|
+
* still that lies about its scene is worse than no still, because it verifies against
|
|
373
|
+
* the wrong header forever.
|
|
374
|
+
*
|
|
375
|
+
* @param {string} expectedSceneId — the node-side `sceneRecord().scene`
|
|
376
|
+
* @param {string} stage
|
|
377
|
+
* @param {object} still — the decoded, normalized bag
|
|
378
|
+
* @returns {object} the same still, when it verifies
|
|
379
|
+
* @throws {Error} FAIL-LOUD on a scene or stage mismatch.
|
|
380
|
+
*/
|
|
381
|
+
function verifyStill(expectedSceneId, stage, still) {
|
|
382
|
+
if (!still || typeof still !== 'object') {
|
|
383
|
+
throw new Error('[FrqtlCaptureHost] the host returned no still for stage ' +
|
|
384
|
+
JSON.stringify(stage) + '. (FAIL-LOUD — never a null still.)');
|
|
385
|
+
}
|
|
386
|
+
if (still.stage !== stage) {
|
|
387
|
+
throw new Error('[FrqtlCaptureHost] asked the host for stage ' + JSON.stringify(stage) +
|
|
388
|
+
' and it returned stage ' + JSON.stringify(still.stage) + '. (FAIL-LOUD.)');
|
|
389
|
+
}
|
|
390
|
+
if (still.scene !== expectedSceneId) {
|
|
391
|
+
throw new Error('[FrqtlCaptureHost] SCENE IDENTITY MISMATCH: the hosted page stamped ' +
|
|
392
|
+
JSON.stringify(still.scene) + ' but this process computed ' +
|
|
393
|
+
JSON.stringify(expectedSceneId) + ' for the same scene. The two are supposed to be ' +
|
|
394
|
+
'equal BY CONSTRUCTION (host attachment extends the node identity with the ' +
|
|
395
|
+
'page-resolved build/projection), so a mismatch means the page is no longer ' +
|
|
396
|
+
'running the scene this handle was opened for. The still is REFUSED rather than ' +
|
|
397
|
+
'filed against the wrong header. (FAIL-LOUD.)');
|
|
398
|
+
}
|
|
399
|
+
return still;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* verifyVideo(expectedSceneId, envelope) — the per-recording fidelity check.
|
|
404
|
+
*
|
|
405
|
+
* Re-runs the OWNER'S `videoEnvelope` over what the page returned (validation — cadence,
|
|
406
|
+
* contiguous indices, encoded fields — never a second stamping), then refuses a recording
|
|
407
|
+
* whose scene is not the one this process computed, for the same reason `verifyStill`
|
|
408
|
+
* refuses a still.
|
|
409
|
+
*
|
|
410
|
+
* @param {string} expectedSceneId — the node-side `sceneRecord().scene`
|
|
411
|
+
* @param {object} envelope — what the host's `record` resolved
|
|
412
|
+
* @returns {object} the re-normalized envelope, when it verifies
|
|
413
|
+
* @throws {Error} FAIL-LOUD on a malformed envelope or a scene mismatch.
|
|
414
|
+
*/
|
|
415
|
+
function verifyVideo(expectedSceneId, envelope) {
|
|
416
|
+
if (!envelope || typeof envelope !== 'object' || envelope.kind !== 'video') {
|
|
417
|
+
throw new Error('[FrqtlCaptureHost] the host returned no video envelope (got ' +
|
|
418
|
+
String(envelope && envelope.kind) + '). (FAIL-LOUD — never a null recording.)');
|
|
419
|
+
}
|
|
420
|
+
var env = CaptureEnvelope.videoEnvelope({
|
|
421
|
+
track: envelope.track, encoded: envelope.encoded, truncated: envelope.truncated
|
|
422
|
+
});
|
|
423
|
+
if (env.track.scene !== expectedSceneId) {
|
|
424
|
+
throw new Error('[FrqtlCaptureHost] SCENE IDENTITY MISMATCH: the hosted page recorded ' +
|
|
425
|
+
'scene ' + JSON.stringify(env.track.scene) + ' but this process computed ' +
|
|
426
|
+
JSON.stringify(expectedSceneId) + ' for the same scene. The recording is REFUSED ' +
|
|
427
|
+
'rather than filed against the wrong header. (FAIL-LOUD.)');
|
|
428
|
+
}
|
|
429
|
+
return env;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// ── Export ────────────────────────────────────────────────────────────────
|
|
433
|
+
|
|
434
|
+
var FrqtlCaptureHost = {
|
|
435
|
+
CAPTURE_HOST_KIND: CAPTURE_HOST_KIND,
|
|
436
|
+
assertCaptureHost: assertCaptureHost,
|
|
437
|
+
IN_PAGE_CAPTURE: IN_PAGE_CAPTURE,
|
|
438
|
+
IN_PAGE_RECORD_BEGIN: IN_PAGE_RECORD_BEGIN,
|
|
439
|
+
IN_PAGE_RECORD_END: IN_PAGE_RECORD_END,
|
|
440
|
+
IN_PAGE_RECORD_ABORT: IN_PAGE_RECORD_ABORT,
|
|
441
|
+
decodeStillEnvelope: decodeStillEnvelope,
|
|
442
|
+
diffSceneRecords: diffSceneRecords,
|
|
443
|
+
verifyStill: verifyStill,
|
|
444
|
+
verifyVideo: verifyVideo,
|
|
445
|
+
registerRecorder: registerRecorder,
|
|
446
|
+
recorder: recorder,
|
|
447
|
+
parkRecording: parkRecording,
|
|
448
|
+
parkedRecording: parkedRecording,
|
|
449
|
+
forgetRecording: forgetRecording
|
|
450
|
+
};
|
|
451
|
+
|
|
452
|
+
export { FrqtlCaptureHost };
|
|
453
|
+
|