@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,33 @@
|
|
|
1
|
+
/* Copyright 2023-2026 by Essam Abadir */
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* lib/shim/render/sun-look.js — the ONE owner of the SUN's look: its render-option bag.
|
|
5
|
+
*
|
|
6
|
+
* The values are the FINAL FX bag EXACTLY as played and captured in an export session
|
|
7
|
+
* (new_sun.png is the primary oracle; sun.png stays the historical dense reference), verbatim from
|
|
8
|
+
* the exported renderState. They are in the write door's shape — a BAG, what
|
|
9
|
+
* `runner.setRenderOptions(bag)` takes — and every key is a capability the three renderer declares.
|
|
10
|
+
*
|
|
11
|
+
* SunLook.SUN_RENDER — the sun's whole look (rung r8-sun's render rows).
|
|
12
|
+
* SunLook.SUN_GODRAYS — the god-ray pair, PICKED from SUN_RENDER: what another scene lights up
|
|
13
|
+
* "at the sun's settings" with (the mirror's 3D mode, the 3D DSE's switch).
|
|
14
|
+
*
|
|
15
|
+
* `godraySource` is NOT here: what the rays are computed from is each borrower's declared choice.
|
|
16
|
+
* The camera is NOT here: a pose is a rung row's view, not a look.
|
|
17
|
+
*
|
|
18
|
+
* Readers: `lib/shim/registry/walkthrough.js` (a named import) and a scene sketch (`SunLook`, the
|
|
19
|
+
* bridge's publication of this module until the game layer imports it). Zero physics: render
|
|
20
|
+
* options are read only by the renderer.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
var SUN_RENDER = Object.freeze({
|
|
25
|
+
shape: 'sphere', colorScheme: 'warmcool',
|
|
26
|
+
godrays: 1.08, godrayDensity: 1.085,
|
|
27
|
+
glowIntensity: 0.93, heat: 0.35, smoke: 0
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
// The pair other scenes light up "at the sun's settings" with — PICKED from SUN_RENDER, never re-typed.
|
|
31
|
+
var SUN_GODRAYS = Object.freeze({ godrays: SUN_RENDER.godrays, godrayDensity: SUN_RENDER.godrayDensity });
|
|
32
|
+
|
|
33
|
+
export { SUN_RENDER, SUN_GODRAYS };
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
/* Copyright 2023-2026 by Essam Abadir */
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* lib/shim/render-state/apply.js — the ONE owner of the renderState contract AND of the procedure
|
|
5
|
+
* that applies a renderState bag to a booted runner.
|
|
6
|
+
*
|
|
7
|
+
* A renderState bag (renderer/seed load-time selections + render rows + camera + backdrop
|
|
8
|
+
* visibility + page controls) is replayed by every host: the GYM rung frame, STUDIO, the bundle's
|
|
9
|
+
* `bootRecipe`, the hosted eyes. They all come through `RenderState.apply` — directly (the bundle,
|
|
10
|
+
* which is its own page) or through the blank page's door `FrqtlPage.applyRenderState`
|
|
11
|
+
* (lib/frqtl/experiments/empty/index.html). A host never restates the procedure.
|
|
12
|
+
*
|
|
13
|
+
* ── Layer 1: API contract (named ES-module exports) ─────────────────────────────────────────────
|
|
14
|
+
*
|
|
15
|
+
* apply(runner, bag, env) → { applied, retained, camera, preset, backdrop }
|
|
16
|
+
* bag — ANY renderState (raw or normalized); normalized here, so a registry bag and a saved
|
|
17
|
+
* project's bag take one path.
|
|
18
|
+
* env — { knownNames: string[] }: `RenderCapabilitiesCommon.knownRenderCapabilityNames(
|
|
19
|
+
* providers)` — the page's renderer registry is a page global, not the runner's.
|
|
20
|
+
* Everything is RESOLVED before anything is written: a row no renderer declares, a camera
|
|
21
|
+
* on a runner with no `cameraControl`, or a camera handle with no `presets` THROWS with the
|
|
22
|
+
* runner untouched.
|
|
23
|
+
* Rows → `partitionRenderRows` against `runner.renderCapabilities()`, then ONE
|
|
24
|
+
* `runner.setRenderOptions(applied)` batch.
|
|
25
|
+
* Camera → `pickCameraReplay`: a preset the booted camera offers (`cameraControl.presets`)
|
|
26
|
+
* → `setPreset`, camera:'preset'; a preset it does not offer is NOT applied,
|
|
27
|
+
* camera:'retained' (the CALLER decides whether that is acceptable); an orbit →
|
|
28
|
+
* `setOrbit`, camera:'orbit'; no camera → null. `preset` names the preset applied
|
|
29
|
+
* or retained (null for an orbit or no camera).
|
|
30
|
+
* Backdrop → `backdropOf(runner)`: a boolean `backdropVisible` →
|
|
31
|
+
* `runner.updateObjectProperty(lo, 'visible', v)`, backdrop:v. An ABSENT
|
|
32
|
+
* `backdropVisible` (null after normalizing) is not written — the scene's own
|
|
33
|
+
* `addBackdrop(…, {visible})` rules — and reports backdrop:null, as does no backdrop LO.
|
|
34
|
+
* `pageControls` are NOT applied here: they are STUDIO's host-specific override.
|
|
35
|
+
*
|
|
36
|
+
* backdropOf(runner) — THE answer to "which LO is the backdrop" for a host: the
|
|
37
|
+
* census LO marked `backdrop === true`, or null. STUDIO's
|
|
38
|
+
* save captures its visibility through it, so capture and
|
|
39
|
+
* replay read one fact.
|
|
40
|
+
* normalizeSavedRenderState(raw) — tolerant normalizer for a LOADED bag (never throws).
|
|
41
|
+
* pickCameraReplay(saved) — pure decision: named preset over raw orbit.
|
|
42
|
+
* partitionRenderRows(rows, declared, known) — apply / retain / throw, before anything is written.
|
|
43
|
+
* renderRowsFromBag(bag) — a { key: value } render-option bag as ordered rows.
|
|
44
|
+
*
|
|
45
|
+
* ── Layer 2: physics behavior ────────────────────────────────────────────────────────────────────
|
|
46
|
+
*
|
|
47
|
+
* Zero physics. Replaying a value has the IDENTICAL effect as a user re-dragging the same control;
|
|
48
|
+
* the runner's own doors (`setRenderOptions`, `cameraControl`, `updateObjectProperty`) do the work.
|
|
49
|
+
*/
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
/** @returns {boolean} */
|
|
53
|
+
function _isPlainObject(v) { return !!v && typeof v === 'object' && !Array.isArray(v); }
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* @param {{camera?: {orbit?:object|null, preset?:string|null}|null}} saved — a normalized bag.
|
|
57
|
+
* @returns {{mode:'preset', name:string}|{mode:'orbit', orbit:object}|null}
|
|
58
|
+
* `null` — no camera was captured (nothing to replay).
|
|
59
|
+
*/
|
|
60
|
+
function pickCameraReplay(saved) {
|
|
61
|
+
if (!saved || !saved.camera) { return null; }
|
|
62
|
+
var cam = saved.camera;
|
|
63
|
+
if (typeof cam.preset === 'string' && cam.preset.length > 0) {
|
|
64
|
+
return { mode: 'preset', name: cam.preset };
|
|
65
|
+
}
|
|
66
|
+
if (cam.orbit && typeof cam.orbit === 'object') {
|
|
67
|
+
return { mode: 'orbit', orbit: cam.orbit };
|
|
68
|
+
}
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Tolerant normalizer for a saved module's `renderState` field — a stored record may be
|
|
74
|
+
* `undefined` (a pre-S6 legacy module), `null`, or a shape from another `version` — NEVER throws;
|
|
75
|
+
* every missing/malformed field degrades to a NEUTRAL default (empty array / null), one `apply`
|
|
76
|
+
* writes nothing for.
|
|
77
|
+
*
|
|
78
|
+
* @param {*} raw
|
|
79
|
+
* @returns {{
|
|
80
|
+
* version: number,
|
|
81
|
+
* renderer: string|null,
|
|
82
|
+
* engine: string|null,
|
|
83
|
+
* seed: string|null,
|
|
84
|
+
* backdropVisible: boolean|null,
|
|
85
|
+
* camera: {orbit:object|null, preset:string|null}|null,
|
|
86
|
+
* render: {key:string, value:*}[],
|
|
87
|
+
* pageControls: {id:string, value:*, bind:{object:string,prop:string}|null}[]
|
|
88
|
+
* }}
|
|
89
|
+
*/
|
|
90
|
+
function normalizeSavedRenderState(raw) {
|
|
91
|
+
var r = _isPlainObject(raw) ? raw : {};
|
|
92
|
+
var camera = null;
|
|
93
|
+
if (_isPlainObject(r.camera)) {
|
|
94
|
+
camera = {
|
|
95
|
+
orbit: _isPlainObject(r.camera.orbit) ? r.camera.orbit : null,
|
|
96
|
+
preset: (typeof r.camera.preset === 'string') ? r.camera.preset : null
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
return {
|
|
100
|
+
version: (typeof r.version === 'number') ? r.version : 1,
|
|
101
|
+
renderer: (typeof r.renderer === 'string' && r.renderer.length > 0) ? r.renderer : null,
|
|
102
|
+
engine: (typeof r.engine === 'string' && r.engine.length > 0) ? r.engine : null,
|
|
103
|
+
seed: (r.seed !== undefined && r.seed !== null && r.seed !== '') ? String(r.seed) : null,
|
|
104
|
+
backdropVisible: (typeof r.backdropVisible === 'boolean') ? r.backdropVisible : null,
|
|
105
|
+
camera: camera,
|
|
106
|
+
render: Array.isArray(r.render) ? r.render.filter(function (row) {
|
|
107
|
+
return _isPlainObject(row) && typeof row.key === 'string' && row.key.length > 0;
|
|
108
|
+
}) : [],
|
|
109
|
+
pageControls: Array.isArray(r.pageControls) ? r.pageControls.filter(function (row) {
|
|
110
|
+
return _isPlainObject(row) && typeof row.id === 'string' && row.id.length > 0;
|
|
111
|
+
}).map(function (row) {
|
|
112
|
+
return {
|
|
113
|
+
id: row.id,
|
|
114
|
+
value: row.value,
|
|
115
|
+
bind: (_isPlainObject(row.bind) && typeof row.bind.object === 'string' &&
|
|
116
|
+
typeof row.bind.prop === 'string') ? { object: row.bind.object, prop: row.bind.prop } : null
|
|
117
|
+
};
|
|
118
|
+
}) : []
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Resolve a bag's `render[]` rows against the renderer that will RECEIVE them, before anything is
|
|
124
|
+
* written. Each row falls in exactly one class:
|
|
125
|
+
* - declared by the target → `apply` (a `{key: value}` bag for ONE
|
|
126
|
+
* `runner.setRenderOptions` call; a later row wins)
|
|
127
|
+
* - declared by another renderer → `retained` (NOT applied; the host decides what to do)
|
|
128
|
+
* - declared by no renderer → THROWS (a typo, or a deleted option)
|
|
129
|
+
*
|
|
130
|
+
* @param {{key:string, value:*}[]} rows
|
|
131
|
+
* @param {string[]} declaredNames — the target's `renderCapabilities()` names
|
|
132
|
+
* @param {string[]} knownNames — `RenderCapabilitiesCommon.knownRenderCapabilityNames(registry)`
|
|
133
|
+
* @returns {{apply: Object<string, *>, retained: {key:string, value:*}[]}}
|
|
134
|
+
* @throws {Error} on a row whose key no registered renderer declares, or bad arguments.
|
|
135
|
+
*/
|
|
136
|
+
function partitionRenderRows(rows, declaredNames, knownNames) {
|
|
137
|
+
if (!Array.isArray(rows) || !Array.isArray(declaredNames) || !Array.isArray(knownNames)) {
|
|
138
|
+
throw new Error('[render-state] partitionRenderRows(rows, declaredNames, knownNames) takes three ' +
|
|
139
|
+
'arrays. (FAIL-LOUD.)');
|
|
140
|
+
}
|
|
141
|
+
var apply = {};
|
|
142
|
+
var retained = [];
|
|
143
|
+
for (var i = 0; i < rows.length; i++) {
|
|
144
|
+
var row = rows[i];
|
|
145
|
+
if (declaredNames.indexOf(row.key) !== -1) {
|
|
146
|
+
apply[row.key] = row.value;
|
|
147
|
+
} else if (knownNames.indexOf(row.key) !== -1) {
|
|
148
|
+
retained.push({ key: row.key, value: row.value });
|
|
149
|
+
} else {
|
|
150
|
+
throw new Error('[render-state] saved render option "' + row.key + '" is declared by no ' +
|
|
151
|
+
'registered renderer (known: [' + knownNames.join(', ') + ']). (FAIL-LOUD: a typo or a ' +
|
|
152
|
+
'deleted option would otherwise silently change the picture.)');
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return { apply: apply, retained: retained };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* renderRowsFromBag(bag) — a render-option BAG (`{ key: value }`, the `runner.setRenderOptions`
|
|
160
|
+
* shape) as the ordered `[{ key, value }]` render rows a renderState carries, in the bag's own key
|
|
161
|
+
* order. The ONE place a bag becomes rows, so a look owned as a bag (lib/shim/render/sun-look.js)
|
|
162
|
+
* reaches a rung's renderState without a hand-typed row list.
|
|
163
|
+
*
|
|
164
|
+
* @param {Object<string, *>} bag
|
|
165
|
+
* @returns {{key:string, value:*}[]}
|
|
166
|
+
* @throws {Error} on a non-object or an array.
|
|
167
|
+
*/
|
|
168
|
+
function renderRowsFromBag(bag) {
|
|
169
|
+
if (!bag || typeof bag !== 'object' || Array.isArray(bag)) {
|
|
170
|
+
throw new Error('[render-state] renderRowsFromBag(bag) takes a plain { key: value } object; got ' +
|
|
171
|
+
(Array.isArray(bag) ? 'an array' : String(bag)) + '. (FAIL-LOUD.)');
|
|
172
|
+
}
|
|
173
|
+
return Object.keys(bag).map(function (k) { return { key: k, value: bag[k] }; });
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** The backdrop LargeObject in the runner's census (the one with `backdrop === true`), or null. */
|
|
177
|
+
function backdropOf(runner) {
|
|
178
|
+
var los = runner && runner.largeObjects;
|
|
179
|
+
if (!Array.isArray(los)) {
|
|
180
|
+
throw new Error('[render-state] RenderState.backdropOf: runner.largeObjects is not the world\'s census ' +
|
|
181
|
+
'(an array) — the backdrop is found there and nowhere else. (FAIL-LOUD.)');
|
|
182
|
+
}
|
|
183
|
+
for (var i = 0; i < los.length; i++) {
|
|
184
|
+
if (los[i] && los[i].backdrop === true) { return los[i]; }
|
|
185
|
+
}
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Apply a renderState bag to a booted runner — see the Layer 1 contract above.
|
|
191
|
+
*
|
|
192
|
+
* @param {object} runner — a booted FrqtlApp (its provider attached, its scene built).
|
|
193
|
+
* @param {*} bag — a renderState bag, raw or normalized.
|
|
194
|
+
* @param {{knownNames:string[]}} env
|
|
195
|
+
* @returns {{applied:Object<string,*>, retained:{key:string,value:*}[],
|
|
196
|
+
* camera:'preset'|'orbit'|'retained'|null, preset:string|null, backdrop:boolean|null}}
|
|
197
|
+
* @throws {Error} on bad arguments, a row no renderer declares, a value its descriptor refuses, a
|
|
198
|
+
* camera on a runner with no camera control, or any write the runner refuses.
|
|
199
|
+
*/
|
|
200
|
+
function apply(runner, bag, env) {
|
|
201
|
+
if (!runner || typeof runner.renderCapabilities !== 'function' || typeof runner.setRenderOptions !== 'function') {
|
|
202
|
+
throw new Error('[render-state] RenderState.apply(runner, bag, env): runner must be a booted FrqtlApp ' +
|
|
203
|
+
'(renderCapabilities + setRenderOptions). (FAIL-LOUD.)');
|
|
204
|
+
}
|
|
205
|
+
if (!env || !Array.isArray(env.knownNames)) {
|
|
206
|
+
throw new Error('[render-state] RenderState.apply(runner, bag, env): env.knownNames must be the page\'s ' +
|
|
207
|
+
'RenderCapabilitiesCommon.knownRenderCapabilityNames(providers) array. (FAIL-LOUD.)');
|
|
208
|
+
}
|
|
209
|
+
var n = normalizeSavedRenderState(bag);
|
|
210
|
+
|
|
211
|
+
// ── RESOLVE everything before anything is written ──
|
|
212
|
+
// Rows — partitioned against the booted renderer.
|
|
213
|
+
var parts = partitionRenderRows(
|
|
214
|
+
n.render,
|
|
215
|
+
runner.renderCapabilities().map(function (c) { return c.name; }),
|
|
216
|
+
env.knownNames
|
|
217
|
+
);
|
|
218
|
+
// Camera — preset over orbit; a preset the booted camera does not offer is reported, not applied.
|
|
219
|
+
var cam = pickCameraReplay(n);
|
|
220
|
+
var ctl = null, camera = null, preset = null;
|
|
221
|
+
if (cam) {
|
|
222
|
+
ctl = runner.cameraControl;
|
|
223
|
+
if (!ctl) {
|
|
224
|
+
throw new Error('[render-state] the bag declares a camera (' + JSON.stringify(n.camera) + ') but the ' +
|
|
225
|
+
'runner has no cameraControl — a declared view on a display with no pose control would look ' +
|
|
226
|
+
'fine and be wrong. (FAIL-LOUD.)');
|
|
227
|
+
}
|
|
228
|
+
if (cam.mode === 'preset') {
|
|
229
|
+
if (!Array.isArray(ctl.presets)) {
|
|
230
|
+
throw new Error('[render-state] the booted camera publishes no `presets` array — every ' +
|
|
231
|
+
'getCameraControl() handle declares the presets it offers. (FAIL-LOUD.)');
|
|
232
|
+
}
|
|
233
|
+
preset = cam.name;
|
|
234
|
+
camera = (ctl.presets.indexOf(cam.name) === -1) ? 'retained' : 'preset';
|
|
235
|
+
} else {
|
|
236
|
+
camera = 'orbit';
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
// Backdrop — the one derivation: the census LO marked `backdrop === true`.
|
|
240
|
+
var lo = backdropOf(runner);
|
|
241
|
+
|
|
242
|
+
// ── WRITE ──
|
|
243
|
+
runner.setRenderOptions(parts.apply); // ONE validated batch
|
|
244
|
+
if (camera === 'preset') { ctl.setPreset(cam.name); }
|
|
245
|
+
else if (camera === 'orbit') { ctl.setOrbit(cam.orbit); }
|
|
246
|
+
var backdrop = null;
|
|
247
|
+
if (lo && n.backdropVisible !== null) {
|
|
248
|
+
runner.updateObjectProperty(lo, 'visible', n.backdropVisible);
|
|
249
|
+
backdrop = n.backdropVisible;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return { applied: parts.apply, retained: parts.retained, camera: camera, preset: preset, backdrop: backdrop };
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
export {
|
|
256
|
+
apply,
|
|
257
|
+
backdropOf,
|
|
258
|
+
normalizeSavedRenderState,
|
|
259
|
+
pickCameraReplay,
|
|
260
|
+
partitionRenderRows,
|
|
261
|
+
renderRowsFromBag
|
|
262
|
+
};
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/* Copyright 2023-2026 by Essam Abadir */
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* lib/shim/render-state/index.js — the ESM door to the renderState contract.
|
|
5
|
+
*
|
|
6
|
+
* The contract and the APPLY procedure have ONE home: `lib/shim/render-state/apply.js`, an ES module
|
|
7
|
+
* the page (through experiments/_shared/boot.js), the parent realm and the sdk bundle all import.
|
|
8
|
+
* This module re-exports its namespace and members by name, so importers (STUDIO's save-state, the
|
|
9
|
+
* bundle codec, the walkthrough registry, the sdk esm entry) read the same object — never a copy.
|
|
10
|
+
*
|
|
11
|
+
* ── Layer 1: API contract ────────────────────────────────────────────────────
|
|
12
|
+
*
|
|
13
|
+
* RenderState — the owner object: RenderState.apply(runner, bag, env).
|
|
14
|
+
* normalizeSavedRenderState(raw) — tolerant normalizer for a LOADED bag (never throws).
|
|
15
|
+
* pickCameraReplay(saved) — pure decision: named preset over raw orbit.
|
|
16
|
+
* partitionRenderRows(rows, declared, known)
|
|
17
|
+
* — resolve render rows against the TARGET renderer.
|
|
18
|
+
* renderRowsFromBag(bag) — a { key: value } bag as ordered [{ key, value }] rows.
|
|
19
|
+
* applySavedUrlOverrides(src, over) — pure URL-string mutation for the load-TIME
|
|
20
|
+
* renderer/seed query params (lives here: the page never
|
|
21
|
+
* builds its own iframe src).
|
|
22
|
+
*
|
|
23
|
+
* ── Layer 2: physics behavior ────────────────────────────────────────────────
|
|
24
|
+
*
|
|
25
|
+
* Zero physics — data shaping over a renderState bag.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
'use strict';
|
|
29
|
+
|
|
30
|
+
// The one owner of the renderState contract and its apply procedure.
|
|
31
|
+
import * as RenderState from './apply.js';
|
|
32
|
+
import { normalizeSavedRenderState, pickCameraReplay, partitionRenderRows, renderRowsFromBag } from './apply.js';
|
|
33
|
+
|
|
34
|
+
// ── applySavedUrlOverrides ───────────────────────────────────────────────────
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Set (or leave untouched) the `renderer`/`seed` query params on an
|
|
38
|
+
* experiment iframe `src` — the load-TIME axis of a saved render-state bag.
|
|
39
|
+
*
|
|
40
|
+
* @param {string} src — an iframe `src` (with or without an existing `?query`).
|
|
41
|
+
* @param {{renderer?:string|null, seed?:number|string|null}} [overrides]
|
|
42
|
+
* @returns {string} `src` with the requested overrides applied. Identical to
|
|
43
|
+
* `src` when `overrides` is absent or every field is falsy/undefined.
|
|
44
|
+
*/
|
|
45
|
+
function applySavedUrlOverrides(src, overrides) {
|
|
46
|
+
if (typeof src !== 'string' || !overrides) { return src; }
|
|
47
|
+
if (Object.prototype.hasOwnProperty.call(overrides, 'egpt3D')) {
|
|
48
|
+
throw new Error('[render-state] applySavedUrlOverrides: `egpt3D` is DELETED — 3D-ness is the scene\'s ' +
|
|
49
|
+
'co-init call (exp.createScene), and the display is `renderer` (three IS the 3D display). (FAIL-LOUD.)');
|
|
50
|
+
}
|
|
51
|
+
var qIdx = src.indexOf('?');
|
|
52
|
+
var base = qIdx === -1 ? src : src.slice(0, qIdx);
|
|
53
|
+
var qs = qIdx === -1 ? '' : src.slice(qIdx + 1);
|
|
54
|
+
var params = new URLSearchParams(qs);
|
|
55
|
+
|
|
56
|
+
if (overrides.renderer) { params.set('renderer', overrides.renderer); }
|
|
57
|
+
if (overrides.seed !== undefined && overrides.seed !== null && overrides.seed !== '') {
|
|
58
|
+
params.set('seed', String(overrides.seed));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
var qsOut = params.toString();
|
|
62
|
+
return qsOut ? (base + '?' + qsOut) : base;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ── Export ────────────────────────────────────────────────────────────────────
|
|
66
|
+
|
|
67
|
+
export {
|
|
68
|
+
RenderState,
|
|
69
|
+
normalizeSavedRenderState,
|
|
70
|
+
pickCameraReplay,
|
|
71
|
+
applySavedUrlOverrides,
|
|
72
|
+
partitionRenderRows,
|
|
73
|
+
renderRowsFromBag
|
|
74
|
+
};
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
/* Copyright 2023-2026 by Essam Abadir */
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* lib/shim/rpc — the request/response call() primitive (gym-controls-as-sugar
|
|
5
|
+
* Layer-A follow-up; gate 5 charter —
|
|
6
|
+
* "the reqId/pending/timeout request/response-call()-over-postMessage pattern
|
|
7
|
+
* is promoted to a SHIM PRIMITIVE the moment a second consumer needs it").
|
|
8
|
+
*
|
|
9
|
+
* Peer of `lib/shim/controls`, `lib/shim/frame-data`, `lib/shim/expr/ast-schema`,
|
|
10
|
+
* `lib/shim/page-params` — same layout convention: named ES-module exports, imported by
|
|
11
|
+
* page-context sugar (`lib/frqtl/parts/FrqtlControlsPanel.js`) and the IDE alike.
|
|
12
|
+
*
|
|
13
|
+
* ── API ──────────────────────────────────────────────────────────────────────
|
|
14
|
+
*
|
|
15
|
+
* makeInAppRpc(dispatch) → { call(fn,args)->Promise, dispose() }
|
|
16
|
+
* Same-realm (same window) calling — `dispatch` runs directly, wrapped in
|
|
17
|
+
* a Promise for a uniform caller shape (no postMessage round-trip; there
|
|
18
|
+
* is no cross-realm boundary to cross).
|
|
19
|
+
*
|
|
20
|
+
* makeChannelRpc(channel, opts) → { call(fn,args)->Promise, dispose() }
|
|
21
|
+
* Cross-realm calling over any `{post,subscribe}` channel (same- or
|
|
22
|
+
* cross-origin — the mechanism does not care which). Builds the
|
|
23
|
+
* reqId/pending/timeout request/response protocol; rejects on timeout
|
|
24
|
+
* (`opts.callTimeoutMs`, default 3000) if the other side never replies —
|
|
25
|
+
* the ONE MUST-HAVE this primitive adds over a bare channel: an
|
|
26
|
+
* unresponsive/not-yet-booted target must not hang a caller's Promise
|
|
27
|
+
* forever.
|
|
28
|
+
*
|
|
29
|
+
* installRpcResponder(channel, dispatch, opts) → unsubscribeFn
|
|
30
|
+
* The REALM/receiving side's counterpart to `makeChannelRpc`: listens on
|
|
31
|
+
* `channel` for calls tagged `opts.tag`, invokes `dispatch(fn,args)`
|
|
32
|
+
* (sync OR Promise-returning — both supported uniformly), replies with
|
|
33
|
+
* the result or error. `dispatch`'s return value must be plain,
|
|
34
|
+
* structured-clone-serializable data (this module does not itself assert
|
|
35
|
+
* that — see the design-note below).
|
|
36
|
+
*
|
|
37
|
+
* makeWindowPostMessageChannel(targetWindow, opts) → {post,subscribe}
|
|
38
|
+
* A ready-made `window.postMessage`/`'message'`-event channel targeting
|
|
39
|
+
* one specific window (an iframe's `contentWindow`, or `window.parent`)
|
|
40
|
+
* — the shape `makeChannelRpc`/`installRpcResponder` accept.
|
|
41
|
+
*
|
|
42
|
+
* `opts.tag` is REQUIRED on `makeChannelRpc`/`installRpcResponder` — a short
|
|
43
|
+
* string namespacing this RPC user's postMessage traffic so two independent
|
|
44
|
+
* RPC users sharing one `window.postMessage` bus (e.g. this primitive AND
|
|
45
|
+
* the notebook realm's own `__frqtlRealm`-tagged traffic) never collide.
|
|
46
|
+
*
|
|
47
|
+
* ── Why this module exists ──────────────────────────────────────────────────
|
|
48
|
+
*
|
|
49
|
+
* `frqtl-controls-panel.js` once built this request/response protocol INLINE; the
|
|
50
|
+
* protocol is a shim primitive so the panel and every later consumer share one
|
|
51
|
+
* reqId/pending/timeout implementation.
|
|
52
|
+
*/
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* makeInAppRpc(dispatch) — same-realm (same window) RPC.
|
|
58
|
+
* @param {(fn: string, args: any[]) => *} dispatch — invoked directly; may
|
|
59
|
+
* return a value or a Promise.
|
|
60
|
+
* @returns {{call: (fn: string, args: any[]) => Promise<*>, dispose: () => void}}
|
|
61
|
+
* @throws {Error} FAIL-LOUD if `dispatch` is not a function.
|
|
62
|
+
*/
|
|
63
|
+
function makeInAppRpc(dispatch) {
|
|
64
|
+
if (typeof dispatch !== 'function') {
|
|
65
|
+
throw new Error('[shim/rpc] makeInAppRpc requires a dispatch function. (FAIL-LOUD.)');
|
|
66
|
+
}
|
|
67
|
+
return {
|
|
68
|
+
call: function (fn, args) {
|
|
69
|
+
try { return Promise.resolve(dispatch(fn, args)); }
|
|
70
|
+
catch (e) { return Promise.reject(e); }
|
|
71
|
+
},
|
|
72
|
+
dispose: function () {}
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* makeChannelRpc(channel, opts) — cross-realm RPC over a `{post,subscribe}`
|
|
78
|
+
* channel (see `makeWindowPostMessageChannel` for a ready-made one).
|
|
79
|
+
* @param {{post: (msg:*) => void, subscribe: (handler:(msg:*)=>void) => (() => void)}} channel
|
|
80
|
+
* @param {object} opts
|
|
81
|
+
* @param {string} opts.tag — REQUIRED: a unique namespace for this RPC's wire traffic.
|
|
82
|
+
* @param {number} [opts.callTimeoutMs=3000] — per-call timeout.
|
|
83
|
+
* @returns {{call: (fn: string, args: any[]) => Promise<*>, dispose: () => void}}
|
|
84
|
+
* @throws {Error} FAIL-LOUD if `channel` lacks post/subscribe, or `opts.tag` is missing.
|
|
85
|
+
*/
|
|
86
|
+
function makeChannelRpc(channel, opts) {
|
|
87
|
+
opts = opts || {};
|
|
88
|
+
if (!channel || typeof channel.post !== 'function' || typeof channel.subscribe !== 'function') {
|
|
89
|
+
throw new Error('[shim/rpc] makeChannelRpc requires a channel with post/subscribe. (FAIL-LOUD.)');
|
|
90
|
+
}
|
|
91
|
+
var tag = opts.tag;
|
|
92
|
+
if (!tag) {
|
|
93
|
+
throw new Error('[shim/rpc] makeChannelRpc requires opts.tag (a unique namespace so ' +
|
|
94
|
+
'multiple RPC users sharing one postMessage bus never collide). (FAIL-LOUD.)');
|
|
95
|
+
}
|
|
96
|
+
var timeoutMs = (typeof opts.callTimeoutMs === 'number' && opts.callTimeoutMs > 0) ? opts.callTimeoutMs : 3000;
|
|
97
|
+
var pending = Object.create(null);
|
|
98
|
+
var reqCounter = 0;
|
|
99
|
+
var unsub = channel.subscribe(function (msg) {
|
|
100
|
+
if (!msg || msg.__rpcTag !== tag || msg.type !== 'rpc:result') { return; }
|
|
101
|
+
var p = pending[msg.reqId];
|
|
102
|
+
if (!p) { return; }
|
|
103
|
+
delete pending[msg.reqId];
|
|
104
|
+
if (msg.error) { p.reject(new Error('[shim/rpc] remote: ' + msg.error)); }
|
|
105
|
+
else { p.resolve(msg.value); }
|
|
106
|
+
});
|
|
107
|
+
return {
|
|
108
|
+
call: function (fn, args) {
|
|
109
|
+
return new Promise(function (resolve, reject) {
|
|
110
|
+
var reqId = ++reqCounter;
|
|
111
|
+
var timer = setTimeout(function () {
|
|
112
|
+
if (!pending[reqId]) { return; } // already settled by a real reply
|
|
113
|
+
delete pending[reqId];
|
|
114
|
+
reject(new Error('[shim/rpc] "' + fn + '" timed out after ' + timeoutMs +
|
|
115
|
+
'ms (tag "' + tag + '"). No responder installed on the other side? ' +
|
|
116
|
+
'(installRpcResponder)'));
|
|
117
|
+
}, timeoutMs);
|
|
118
|
+
pending[reqId] = {
|
|
119
|
+
resolve: function (v) { clearTimeout(timer); resolve(v); },
|
|
120
|
+
reject: function (e) { clearTimeout(timer); reject(e); }
|
|
121
|
+
};
|
|
122
|
+
channel.post({ __rpcTag: tag, type: 'rpc:call', reqId: reqId, fn: fn, args: args });
|
|
123
|
+
});
|
|
124
|
+
},
|
|
125
|
+
dispose: function () { if (typeof unsub === 'function') { unsub(); } }
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* installRpcResponder(channel, dispatch, opts) — the REALM/receiving side.
|
|
131
|
+
* @param {{post: Function, subscribe: Function}} channel
|
|
132
|
+
* @param {(fn: string, args: any[]) => *} dispatch — may return a value or a Promise.
|
|
133
|
+
* @param {object} opts
|
|
134
|
+
* @param {string} opts.tag — MUST match the caller's `makeChannelRpc` tag.
|
|
135
|
+
* @returns {() => void} unsubscribe function.
|
|
136
|
+
* @throws {Error} FAIL-LOUD if `channel` lacks post/subscribe, or `opts.tag` is missing.
|
|
137
|
+
*/
|
|
138
|
+
function installRpcResponder(channel, dispatch, opts) {
|
|
139
|
+
opts = opts || {};
|
|
140
|
+
if (!channel || typeof channel.post !== 'function' || typeof channel.subscribe !== 'function') {
|
|
141
|
+
throw new Error('[shim/rpc] installRpcResponder requires a channel with post/subscribe. (FAIL-LOUD.)');
|
|
142
|
+
}
|
|
143
|
+
if (typeof dispatch !== 'function') {
|
|
144
|
+
throw new Error('[shim/rpc] installRpcResponder requires a dispatch function. (FAIL-LOUD.)');
|
|
145
|
+
}
|
|
146
|
+
var tag = opts.tag;
|
|
147
|
+
if (!tag) {
|
|
148
|
+
throw new Error('[shim/rpc] installRpcResponder requires opts.tag. (FAIL-LOUD.)');
|
|
149
|
+
}
|
|
150
|
+
return channel.subscribe(function (msg) {
|
|
151
|
+
if (!msg || msg.__rpcTag !== tag || msg.type !== 'rpc:call') { return; }
|
|
152
|
+
var reqId = msg.reqId;
|
|
153
|
+
Promise.resolve().then(function () {
|
|
154
|
+
return dispatch(msg.fn, msg.args);
|
|
155
|
+
}).then(function (value) {
|
|
156
|
+
channel.post({ __rpcTag: tag, type: 'rpc:result', reqId: reqId, value: value });
|
|
157
|
+
}, function (e) {
|
|
158
|
+
channel.post({
|
|
159
|
+
__rpcTag: tag, type: 'rpc:result', reqId: reqId,
|
|
160
|
+
error: (e && e.message) ? e.message : String(e)
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* makeWindowPostMessageChannel(targetWindow, opts) — a ready-made
|
|
168
|
+
* `{post,subscribe}` channel over `window.postMessage`/`'message'` events,
|
|
169
|
+
* scoped to one specific `targetWindow` (an iframe's `contentWindow`, or
|
|
170
|
+
* `window.parent`) — messages from any OTHER source are ignored.
|
|
171
|
+
* @param {Window} targetWindow — the window to post to / accept messages from.
|
|
172
|
+
* @param {object} [opts]
|
|
173
|
+
* @param {Window} [opts.selfWindow] — defaults to the global `window`.
|
|
174
|
+
* @returns {{post: Function, subscribe: Function}}
|
|
175
|
+
* @throws {Error} FAIL-LOUD if `targetWindow` or the self window is missing.
|
|
176
|
+
*/
|
|
177
|
+
function makeWindowPostMessageChannel(targetWindow, opts) {
|
|
178
|
+
opts = opts || {};
|
|
179
|
+
var selfWindow = opts.selfWindow || (typeof window !== 'undefined' ? window : null);
|
|
180
|
+
if (!targetWindow) {
|
|
181
|
+
throw new Error('[shim/rpc] makeWindowPostMessageChannel requires targetWindow. (FAIL-LOUD.)');
|
|
182
|
+
}
|
|
183
|
+
if (!selfWindow) {
|
|
184
|
+
throw new Error('[shim/rpc] makeWindowPostMessageChannel requires a self window (pass ' +
|
|
185
|
+
'opts.selfWindow outside a browser). (FAIL-LOUD.)');
|
|
186
|
+
}
|
|
187
|
+
return {
|
|
188
|
+
post: function (msg) { targetWindow.postMessage(msg, '*'); },
|
|
189
|
+
subscribe: function (handler) {
|
|
190
|
+
var listener = function (ev) {
|
|
191
|
+
if (ev.source !== targetWindow) { return; }
|
|
192
|
+
handler(ev.data);
|
|
193
|
+
};
|
|
194
|
+
selfWindow.addEventListener('message', listener);
|
|
195
|
+
return function () { selfWindow.removeEventListener('message', listener); };
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// ── Export ────────────────────────────────────────────────────────────────────
|
|
201
|
+
|
|
202
|
+
export {
|
|
203
|
+
makeInAppRpc,
|
|
204
|
+
makeChannelRpc,
|
|
205
|
+
installRpcResponder,
|
|
206
|
+
makeWindowPostMessageChannel
|
|
207
|
+
};
|