@descix/frqtl-node 0.6.1-p3.4 → 0.7.0-p0
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 +3 -3
- package/README.md +4 -3
- package/dist/dist-manifest.json +26 -56
- package/dist/frqtl/experiments/_shared/empty-sketch.js +17 -0
- package/dist/frqtl/objects/setup/setupBlackbodySubstrate.js +6 -6
- package/dist/frqtl/objects/setup/setupFxSmoke.js +2 -2
- package/dist/frqtl/objects/setup/setupG3Collide.js +1 -1
- package/dist/frqtl/objects/setup/setupSphereEmitter3D.js +2 -2
- package/dist/frqtl/objects/v1-replacements.js +1 -1
- package/dist/frqtl/parts/OvenBox.js +2 -2
- package/dist/frqtl/parts/SphereEmitter.js +1 -1
- package/dist/shim/engine/router.js +18 -6
- package/dist/shim/engine/site-assemble.mjs +268 -0
- package/dist/shim/engine/site-graph.js +42 -14
- package/dist/shim/host/device.js +1 -1
- package/dist/shim/index.js +3 -7
- package/dist/shim/notebook/catalog.js +2 -2
- package/dist/shim/notebook/format.js +1 -1
- package/dist/shim/notebook/knob.js +1 -1
- package/dist/shim/notebook/notebook-knobs.js +1 -1
- package/dist/shim/project/specifiers.js +8 -8
- package/dist/shim/registry/copy-gate.js +3 -3
- package/dist/shim/registry/index.js +9 -12
- package/dist/shim/registry/roster-link.js +142 -0
- package/dist/shim/registry/roster.js +595 -0
- package/dist/shim/registry/scene-source.js +80 -105
- package/dist/shim/registry/studio-landing.js +15 -7
- package/dist/shim/render/sun-look.js +1 -2
- package/llms.txt +4 -2
- package/package.json +1 -1
- package/dist/frqtl/scenes/absorbing-wall-3d.js +0 -37
- package/dist/frqtl/scenes/absorbing-wall.js +0 -36
- package/dist/frqtl/scenes/atomic-model.js +0 -91
- package/dist/frqtl/scenes/big-bang.js +0 -49
- package/dist/frqtl/scenes/blackbody.js +0 -84
- package/dist/frqtl/scenes/circuit-sat.js +0 -131
- package/dist/frqtl/scenes/collision-matrix-3d.js +0 -110
- package/dist/frqtl/scenes/counting-wall-3d.js +0 -40
- package/dist/frqtl/scenes/counting-wall.js +0 -42
- package/dist/frqtl/scenes/double-slit.js +0 -34
- package/dist/frqtl/scenes/dse-one-emitter.js +0 -200
- package/dist/frqtl/scenes/dse-one-slit.js +0 -118
- package/dist/frqtl/scenes/dse-photoelectric.js +0 -74
- package/dist/frqtl/scenes/dse-tunnel-rebuild.js +0 -279
- package/dist/frqtl/scenes/dse-two-emitters.js +0 -201
- package/dist/frqtl/scenes/dse-two-slits.js +0 -136
- package/dist/frqtl/scenes/empty.js +0 -17
- package/dist/frqtl/scenes/laser-buildup.js +0 -56
- package/dist/frqtl/scenes/mirror-3d.js +0 -60
- package/dist/frqtl/scenes/mirror-route.js +0 -164
- package/dist/frqtl/scenes/mirror.js +0 -42
- package/dist/frqtl/scenes/omni-3d.js +0 -30
- package/dist/frqtl/scenes/omni-source.js +0 -25
- package/dist/frqtl/scenes/self-interference.js +0 -42
- package/dist/frqtl/scenes/smoking-gun.js +0 -126
- package/dist/frqtl/scenes/straight-walk.js +0 -25
- package/dist/frqtl/scenes/sun.js +0 -42
- package/dist/frqtl/scenes/two-source.js +0 -42
- package/dist/frqtl/scenes/wave.js +0 -29
- package/dist/shim/engine/artifact-shell.js +0 -211
- package/dist/shim/registry/exhibits.js +0 -436
- package/dist/shim/registry/scene-roster.js +0 -35
- package/dist/shim/registry/showcase-link.js +0 -61
- package/dist/shim/registry/walkthrough.js +0 -752
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/* Copyright 2023-2026 by Essam Abadir */
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* roster-link.js — THE loader of a host's roster: the one reader of `<link rel="frqtl-roster" href>` and the one
|
|
5
|
+
* fetch of the file it names. A surface never fetches a roster itself; the host loads it once per document and
|
|
6
|
+
* passes the bag down.
|
|
7
|
+
*
|
|
8
|
+
* ── Layer 1: API contract ────────────────────────────────────────────────────
|
|
9
|
+
*
|
|
10
|
+
* ROSTER_LINK_REL 'frqtl-roster'
|
|
11
|
+
* ROSTER_CACHE_BUCKET_MS the width of the cache-busting bucket a request URL carries on http(s)
|
|
12
|
+
* rosterLinksOfHtml(html) → [{ tag, href }] for every `<link rel="frqtl-roster">` in a document's TEXT (an assembler,
|
|
13
|
+
* a server or a harness reading HTML it has not parsed; `href` decoded, null when absent).
|
|
14
|
+
* The one reader of the link in text; a parsed document goes through rosterHrefOf.
|
|
15
|
+
* rosterHrefOf(doc) → the link's absolute href. THROWS on no link, a second link, a relative href (the
|
|
16
|
+
* store-shell reason) or an href with a query or a hash.
|
|
17
|
+
* rosterRequestUrl(href, now)
|
|
18
|
+
* → the URL to request: `href?b=<floor(now / ROSTER_CACHE_BUCKET_MS)>` on http(s), `href`
|
|
19
|
+
* unchanged on file: (node). Any other scheme THROWS.
|
|
20
|
+
* loadRoster(href, { fetch, now })
|
|
21
|
+
* → Promise of normalizeRoster(json, href). REJECTS naming the URL on a failed read (a
|
|
22
|
+
* non-200 names its status), an empty body or invalid JSON.
|
|
23
|
+
* sceneOriginRefusalOf(url, { pageOrigin, roster })
|
|
24
|
+
* → null when a `?scene=` URL's origin is the page's or the roster's; else the refusal text
|
|
25
|
+
* naming the rule and both allowed origins.
|
|
26
|
+
*
|
|
27
|
+
* ── Layer 2: physics behavior ────────────────────────────────────────────────
|
|
28
|
+
*
|
|
29
|
+
* Zero physics — a file read.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
import { normalizeRoster, rosterHrefRefusalOf, assertRosterBag } from './roster.js';
|
|
33
|
+
import { readText } from '../project/read-text.js';
|
|
34
|
+
|
|
35
|
+
/** The `rel` of the one link that names a document's roster. */
|
|
36
|
+
export const ROSTER_LINK_REL = 'frqtl-roster';
|
|
37
|
+
|
|
38
|
+
/** Five minutes. Why: the edge caches the uploaded roster for 1 h; this query and the constant are deleted when
|
|
39
|
+
* the roster uploads with cache_control no-cache (DeSciXCloud#103). */
|
|
40
|
+
export const ROSTER_CACHE_BUCKET_MS = 300000;
|
|
41
|
+
|
|
42
|
+
const LINK_SHAPE = '<link rel="' + ROSTER_LINK_REL + '" href="https://…">';
|
|
43
|
+
|
|
44
|
+
const LINK_TAG = new RegExp('<link\\b[^>]*\\brel=["\']' + ROSTER_LINK_REL + '["\'][^>]*>', 'gi');
|
|
45
|
+
const HREF_ATTR = /\shref=(["'])([^"']*)\1/i;
|
|
46
|
+
const decodeAttr = (v) => v.replace(/"/g, '"').replace(/'/g, "'").replace(/</g, '<').replace(/>/g, '>').replace(/&/g, '&');
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* @param {string} html a document's text
|
|
50
|
+
* @returns {Array<{tag: string, href: string|null}>} every roster link tag in it, in order, with its decoded href
|
|
51
|
+
*/
|
|
52
|
+
export function rosterLinksOfHtml(html) {
|
|
53
|
+
if (typeof html !== 'string') {
|
|
54
|
+
throw new Error('[roster-link] rosterLinksOfHtml(html) takes the document\'s text; got ' + typeof html + '. (FAIL-LOUD.)');
|
|
55
|
+
}
|
|
56
|
+
return (html.match(LINK_TAG) || []).map((tag) => {
|
|
57
|
+
const m = tag.match(HREF_ATTR);
|
|
58
|
+
return { tag, href: m ? decodeAttr(m[2]) : null };
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* @param {Document} doc
|
|
64
|
+
* @returns {string} the roster link's absolute href
|
|
65
|
+
* @throws {Error} no link, two links, or an href that is not an absolute plain file URL
|
|
66
|
+
*/
|
|
67
|
+
export function rosterHrefOf(doc) {
|
|
68
|
+
if (!doc || typeof doc.querySelectorAll !== 'function') {
|
|
69
|
+
throw new Error('[roster-link] rosterHrefOf(doc) takes a document; got ' + String(doc) + '. (FAIL-LOUD.)');
|
|
70
|
+
}
|
|
71
|
+
const links = Array.from(doc.querySelectorAll('link[rel~="' + ROSTER_LINK_REL + '"]'));
|
|
72
|
+
if (links.length === 0) {
|
|
73
|
+
throw new Error('[roster-link] this document has no ' + LINK_SHAPE + ' — STUDIO and the GYM read their scene ' +
|
|
74
|
+
'list from the roster it names; the host writes the link into the page. (FAIL-LOUD.)');
|
|
75
|
+
}
|
|
76
|
+
if (links.length > 1) {
|
|
77
|
+
throw new Error('[roster-link] this document has ' + links.length + ' ' + LINK_SHAPE + ' links; a document ' +
|
|
78
|
+
'names exactly one roster. (FAIL-LOUD.)');
|
|
79
|
+
}
|
|
80
|
+
const href = links[0].getAttribute('href');
|
|
81
|
+
const why = rosterHrefRefusalOf(href);
|
|
82
|
+
if (why) { throw new Error('[roster-link] ' + LINK_SHAPE + ': ' + why + ' (FAIL-LOUD.)'); }
|
|
83
|
+
return href;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* @param {string} href an absolute roster URL
|
|
88
|
+
* @param {number} now epoch milliseconds
|
|
89
|
+
* @returns {string} the URL to request
|
|
90
|
+
*/
|
|
91
|
+
export function rosterRequestUrl(href, now) {
|
|
92
|
+
const why = rosterHrefRefusalOf(href);
|
|
93
|
+
if (why) { throw new Error('[roster-link] rosterRequestUrl(href, now): ' + why + ' (FAIL-LOUD.)'); }
|
|
94
|
+
if (!Number.isFinite(now)) {
|
|
95
|
+
throw new Error('[roster-link] rosterRequestUrl(href, now): now is epoch milliseconds (got ' + String(now) + '). (FAIL-LOUD.)');
|
|
96
|
+
}
|
|
97
|
+
const protocol = new URL(href).protocol;
|
|
98
|
+
if (protocol === 'file:') { return href; }
|
|
99
|
+
return href + '?b=' + Math.floor(now / ROSTER_CACHE_BUCKET_MS);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* @param {string} href an absolute roster URL (rosterHrefOf in a page; a file: URL in node)
|
|
104
|
+
* @param {{fetch?: function, now?: number}} [opts] fetch is injectable; now defaults to Date.now()
|
|
105
|
+
* @returns {Promise<object>} the normalized roster bag
|
|
106
|
+
*/
|
|
107
|
+
export function loadRoster(href, opts) {
|
|
108
|
+
return Promise.resolve().then(() => {
|
|
109
|
+
const o = opts || {};
|
|
110
|
+
const url = rosterRequestUrl(href, o.now === undefined ? Date.now() : o.now);
|
|
111
|
+
return readText(url, { fetch: o.fetch }).then((text) => {
|
|
112
|
+
if (text.trim() === '') { throw new Error('[roster-link] roster ' + url + ' is empty. (FAIL-LOUD.)'); }
|
|
113
|
+
let json = null;
|
|
114
|
+
try { json = JSON.parse(text); }
|
|
115
|
+
catch (e) { throw new Error('[roster-link] roster ' + url + ' is not JSON: ' + e.message + ' (FAIL-LOUD.)'); }
|
|
116
|
+
return normalizeRoster(json, href);
|
|
117
|
+
}, (e) => {
|
|
118
|
+
throw new Error('[roster-link] loading the roster failed: ' + e.message + ' (FAIL-LOUD.)');
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* @param {string} url the `?scene=` value, resolved (absolute)
|
|
125
|
+
* @param {{pageOrigin: string, roster: object}} ctx
|
|
126
|
+
* @returns {string|null} null when allowed; else the refusal text
|
|
127
|
+
*/
|
|
128
|
+
export function sceneOriginRefusalOf(url, ctx) {
|
|
129
|
+
const c = ctx || {};
|
|
130
|
+
assertRosterBag(c.roster, 'sceneOriginRefusalOf(url, { pageOrigin, roster })');
|
|
131
|
+
if (typeof c.pageOrigin !== 'string' || c.pageOrigin === '') {
|
|
132
|
+
throw new Error('[roster-link] sceneOriginRefusalOf(url, { pageOrigin, roster }): pageOrigin is the page\'s ' +
|
|
133
|
+
'location.origin (got ' + JSON.stringify(c.pageOrigin) + '). (FAIL-LOUD.)');
|
|
134
|
+
}
|
|
135
|
+
const rosterOrigin = new URL(c.roster.href).origin;
|
|
136
|
+
let origin = null;
|
|
137
|
+
try { origin = new URL(url).origin; } catch (e) { origin = null; }
|
|
138
|
+
// An opaque origin (a file: or data: URL serializes it as "null") equals nothing, itself included.
|
|
139
|
+
if (origin !== null && origin !== 'null' && (origin === c.pageOrigin || origin === rosterOrigin)) { return null; }
|
|
140
|
+
return 'a scene opens from this page\'s origin (' + c.pageOrigin + ') or its roster\'s origin (' + rosterOrigin +
|
|
141
|
+
'); ' + JSON.stringify(url) + ' is ' + (origin === null ? 'not an absolute URL' : 'on ' + origin) + '.';
|
|
142
|
+
}
|