@descix/frqtl-node 0.6.0-p1.1 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +2 -2
- package/README.md +6 -4
- package/dist/dist-manifest.json +71 -65
- package/dist/eyes/index.js +5 -2
- package/dist/fat/pkg-node/package.json +2 -10
- package/dist/frqtl/compat/providers/wasm-engine.js +3 -4
- package/dist/frqtl/compat/renderer/providers/p5-provider.js +1 -1
- package/dist/frqtl/objects/FrqtlApp.js +33 -6
- package/dist/frqtl/objects/RecipeContext.js +284 -13
- package/dist/frqtl/objects/constants/WallBehaviors.js +1 -1
- package/dist/frqtl/objects/instruments/capture.js +1 -24
- package/dist/frqtl/objects/instruments/hosted-eyes.js +216 -29
- package/dist/frqtl/objects/objects/makeControlsForObject.js +5 -5
- package/dist/frqtl/objects/record/Record.js +346 -0
- package/dist/frqtl/objects/record/open-record.js +22 -0
- package/dist/frqtl/objects/schema/engine-scopes.js +2 -2
- package/dist/frqtl/objects/schema/scopes.js +2 -2
- package/dist/frqtl/objects/setup/_worldInit.js +1 -2
- package/dist/frqtl/objects/setup/setupBlackbodySubstrate.js +1 -2
- package/dist/frqtl/parts/CameraControls.js +8 -0
- package/dist/frqtl/parts/DetectorWall.js +1 -1
- package/dist/frqtl/parts/FrqtlVision.js +21 -6
- package/dist/frqtl/parts/GraphingDetectorWall.js +7 -7
- package/dist/frqtl/parts/LargeObject.js +2 -2
- package/dist/frqtl/parts/LiveChart.js +2 -2
- package/dist/frqtl/parts/LogicGate.js +3 -3
- package/dist/frqtl/parts/OvenBox.js +1 -1
- package/dist/frqtl/parts/PointSource.js +1 -1
- package/dist/frqtl/parts/SlitLightSource.js +1 -1
- package/dist/frqtl/parts/SlitScreen.js +1 -1
- package/dist/frqtl/parts/SphereEmitter.js +12 -12
- package/dist/frqtl/parts/Trace.js +1 -1
- package/dist/frqtl/parts/Tunnel.js +11 -11
- package/dist/frqtl/parts/Wall.js +20 -10
- package/dist/frqtl/scenes/circuit-sat.js +1 -0
- package/dist/frqtl/scenes/dse-one-emitter.js +32 -34
- package/dist/frqtl/scenes/dse-two-emitters.js +33 -34
- package/dist/frqtl/scenes/mirror-route.js +96 -79
- package/dist/shim/bundle/index.js +118 -96
- package/dist/shim/bundle/indexHtml.js +66 -39
- package/dist/shim/controls/index.js +1 -1
- package/dist/shim/engine/artifact-shell.js +1 -1
- package/dist/shim/engine/router.js +3 -3
- package/dist/shim/host/host-view.js +228 -0
- package/dist/shim/host/index.js +18 -0
- package/dist/shim/index.js +3 -5
- package/dist/shim/notebook/format.js +1 -1
- package/dist/shim/notebook/kernel.js +1 -1
- package/dist/shim/notebook/setup-builder.js +2 -2
- package/dist/shim/notebook/store.js +2 -3
- package/dist/shim/package.json +1 -0
- package/dist/shim/page-params/index.js +2 -30
- package/dist/shim/project/fork.js +293 -0
- package/dist/shim/project/identity.js +3 -1
- package/dist/shim/project/index.js +16 -3
- package/dist/shim/project/link.js +224 -46
- package/dist/shim/project/manifest.js +151 -0
- package/dist/shim/project/parts-writer.js +68 -7
- package/dist/shim/project/read-text.js +43 -0
- package/dist/shim/project/record-format.js +345 -0
- package/dist/shim/project/specifiers.js +104 -15
- package/dist/shim/registry/exhibits.js +15 -34
- package/dist/shim/registry/index.js +3 -3
- package/dist/shim/registry/scene-roster.js +35 -0
- package/dist/shim/registry/scene-source.js +48 -64
- package/dist/shim/registry/walkthrough.js +6 -6
- package/dist/shim/render-state/apply.js +66 -31
- package/dist/shim/render-state/index.js +6 -2
- package/dist/shim/runtime/index.js +4 -4
- package/dist/shim/storage/codec-chains.js +88 -0
- package/dist/shim/storage/folders.js +205 -0
- package/dist/shim/storage/index.js +64 -8
- package/dist/shim/storage/make-namespace-store.js +5 -5
- package/llms.txt +4 -3
- package/package.json +19 -1
- package/dist/shim/bundle/frqnb.js +0 -112
- package/dist/shim/bundle/manifest.js +0 -95
- package/dist/shim/registry/experiments.js +0 -215
- package/dist/shim/storage/modules.js +0 -249
- package/dist/shim/storage/projects.js +0 -150
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* that applies a renderState bag to a booted runner.
|
|
6
6
|
*
|
|
7
7
|
* A renderState bag (renderer/seed load-time selections + render rows + camera + backdrop
|
|
8
|
-
* visibility
|
|
8
|
+
* visibility) is replayed by every host: the GYM rung frame, STUDIO, the bundle's
|
|
9
9
|
* `bootRecipe`, the hosted eyes. They all come through `RenderState.apply` — directly (the bundle,
|
|
10
10
|
* which is its own page) or through the blank page's door `FrqtlPage.applyRenderState`
|
|
11
11
|
* (lib/frqtl/experiments/empty/index.html). A host never restates the procedure.
|
|
@@ -37,8 +37,13 @@
|
|
|
37
37
|
* census LO marked `backdrop === true`, or null. STUDIO's
|
|
38
38
|
* save captures its visibility through it, so capture and
|
|
39
39
|
* replay read one fact.
|
|
40
|
-
*
|
|
41
|
-
*
|
|
40
|
+
* RENDER_STATE_KEYS — THE key table of a bag, in the order the normalized bag is written;
|
|
41
|
+
* each row normalizes its own value.
|
|
42
|
+
* RENDER_STATE_VERSION — 1, the one version a bag may declare.
|
|
43
|
+
* normalizeSavedRenderState(raw) — a bag (or null: no boot bag) → the full bag off RENDER_STATE_KEYS.
|
|
44
|
+
* THROWS on a key outside the table (naming it and the table), on
|
|
45
|
+
* another version, on page controls (author code now), and on a
|
|
46
|
+
* value that is neither a plain object nor null.
|
|
42
47
|
* pickCameraReplay(saved) — pure decision: named preset over raw orbit.
|
|
43
48
|
* partitionRenderRows(rows, declared, known) — apply / retain / throw, before anything is written.
|
|
44
49
|
* renderRowsFromBag(bag) — a { key: value } render-option bag as ordered rows.
|
|
@@ -70,13 +75,42 @@ function pickCameraReplay(saved) {
|
|
|
70
75
|
return null;
|
|
71
76
|
}
|
|
72
77
|
|
|
78
|
+
/** The one version a renderState bag may declare (an absent version is this one). */
|
|
79
|
+
var RENDER_STATE_VERSION = 1;
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* THE key table of a renderState bag, in the order the normalized bag is written. Each row's `normalize(value)`
|
|
83
|
+
* takes the raw value (undefined when the key is absent) to the stored form; an absent or malformed value is the
|
|
84
|
+
* neutral one (null / []), which `apply` writes nothing for.
|
|
85
|
+
* @type {ReadonlyArray<{key: string, normalize: function(*): *}>}
|
|
86
|
+
*/
|
|
87
|
+
var RENDER_STATE_KEYS = Object.freeze([
|
|
88
|
+
{ key: 'version', normalize: function (v) {
|
|
89
|
+
if (v === undefined || v === RENDER_STATE_VERSION) { return RENDER_STATE_VERSION; }
|
|
90
|
+
throw new Error('[render-state] renderState.version ' + JSON.stringify(v) + ' — this owner reads version ' +
|
|
91
|
+
RENDER_STATE_VERSION + ' only. (FAIL-LOUD.)');
|
|
92
|
+
} },
|
|
93
|
+
{ key: 'renderer', normalize: function (v) { return (typeof v === 'string' && v.length > 0) ? v : null; } },
|
|
94
|
+
{ key: 'engine', normalize: function (v) { return (typeof v === 'string' && v.length > 0) ? v : null; } },
|
|
95
|
+
{ key: 'seed', normalize: function (v) { return (v !== undefined && v !== null && v !== '') ? String(v) : null; } },
|
|
96
|
+
{ key: 'backdropVisible', normalize: function (v) { return (typeof v === 'boolean') ? v : null; } },
|
|
97
|
+
{ key: 'camera', normalize: function (v) {
|
|
98
|
+
if (!_isPlainObject(v)) { return null; }
|
|
99
|
+
return { orbit: _isPlainObject(v.orbit) ? v.orbit : null, preset: (typeof v.preset === 'string') ? v.preset : null };
|
|
100
|
+
} },
|
|
101
|
+
{ key: 'render', normalize: function (v) {
|
|
102
|
+
return Array.isArray(v) ? v.filter(function (row) {
|
|
103
|
+
return _isPlainObject(row) && typeof row.key === 'string' && row.key.length > 0;
|
|
104
|
+
}) : [];
|
|
105
|
+
} }
|
|
106
|
+
].map(Object.freeze));
|
|
107
|
+
|
|
73
108
|
/**
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
* writes nothing for.
|
|
109
|
+
* The full renderState bag of `raw`, built off RENDER_STATE_KEYS: a bag (a host's capture, a registry rung's, a
|
|
110
|
+
* stored folder row's, an export's boot bag) or `null` (no boot bag — the one-file Import) → every key, in table
|
|
111
|
+
* order. An owner refuses what is not in its contract: a silent drop would be a silent fallback.
|
|
78
112
|
*
|
|
79
|
-
* @param {
|
|
113
|
+
* @param {object|null} raw
|
|
80
114
|
* @returns {{
|
|
81
115
|
* version: number,
|
|
82
116
|
* renderer: string|null,
|
|
@@ -86,34 +120,33 @@ function pickCameraReplay(saved) {
|
|
|
86
120
|
* camera: {orbit:object|null, preset:string|null}|null,
|
|
87
121
|
* render: {key:string, value:*}[]
|
|
88
122
|
* }}
|
|
89
|
-
* @throws {Error} a
|
|
90
|
-
*
|
|
123
|
+
* @throws {Error} a key outside RENDER_STATE_KEYS (named, beside the table); a version other than
|
|
124
|
+
* RENDER_STATE_VERSION; a bag carrying `pageControls` — page controls are declared in the sketch's own code, so a
|
|
125
|
+
* saved bag never carries them (migrate the project: move the controls into its sketch); and a value that is
|
|
126
|
+
* neither a plain object nor null (`undefined` included: a caller with no bag passes null).
|
|
91
127
|
*/
|
|
92
128
|
function normalizeSavedRenderState(raw) {
|
|
93
|
-
|
|
94
|
-
|
|
129
|
+
if (raw !== null && !_isPlainObject(raw)) {
|
|
130
|
+
throw new Error('[render-state] a renderState is a bag (a plain object) or null for none — got ' +
|
|
131
|
+
(raw === undefined ? 'undefined' : Array.isArray(raw) ? 'an array' : typeof raw) + '. (FAIL-LOUD.)');
|
|
132
|
+
}
|
|
133
|
+
var r = raw || {};
|
|
134
|
+
if (Object.prototype.hasOwnProperty.call(r, 'pageControls')) {
|
|
135
|
+
var rows = Array.isArray(r.pageControls) ? r.pageControls : [];
|
|
95
136
|
throw new Error('[render-state] this saved project carries page controls (renderState.pageControls: ' +
|
|
96
|
-
|
|
137
|
+
(rows.map(function (row) { return _isPlainObject(row) ? String(row.id) : '?'; }).join(', ') || 'an empty list') +
|
|
97
138
|
') — page controls are declared in the sketch\'s own code; move them into the sketch and save again. (FAIL-LOUD.)');
|
|
98
139
|
}
|
|
99
|
-
var
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
engine: (typeof r.engine === 'string' && r.engine.length > 0) ? r.engine : null,
|
|
110
|
-
seed: (r.seed !== undefined && r.seed !== null && r.seed !== '') ? String(r.seed) : null,
|
|
111
|
-
backdropVisible: (typeof r.backdropVisible === 'boolean') ? r.backdropVisible : null,
|
|
112
|
-
camera: camera,
|
|
113
|
-
render: Array.isArray(r.render) ? r.render.filter(function (row) {
|
|
114
|
-
return _isPlainObject(row) && typeof row.key === 'string' && row.key.length > 0;
|
|
115
|
-
}) : []
|
|
116
|
-
};
|
|
140
|
+
var known = RENDER_STATE_KEYS.map(function (row) { return row.key; });
|
|
141
|
+
Object.keys(r).forEach(function (k) {
|
|
142
|
+
if (known.indexOf(k) === -1) {
|
|
143
|
+
throw new Error('[render-state] renderState has no key "' + k + '" — its keys (RENDER_STATE_KEYS): ' +
|
|
144
|
+
known.join(', ') + '. (FAIL-LOUD.)');
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
var out = {};
|
|
148
|
+
RENDER_STATE_KEYS.forEach(function (row) { out[row.key] = row.normalize(r[row.key]); });
|
|
149
|
+
return out;
|
|
117
150
|
}
|
|
118
151
|
|
|
119
152
|
/**
|
|
@@ -252,6 +285,8 @@ function apply(runner, bag, env) {
|
|
|
252
285
|
export {
|
|
253
286
|
apply,
|
|
254
287
|
backdropOf,
|
|
288
|
+
RENDER_STATE_KEYS,
|
|
289
|
+
RENDER_STATE_VERSION,
|
|
255
290
|
normalizeSavedRenderState,
|
|
256
291
|
pickCameraReplay,
|
|
257
292
|
partitionRenderRows,
|
|
@@ -11,7 +11,9 @@
|
|
|
11
11
|
* ── Layer 1: API contract ────────────────────────────────────────────────────
|
|
12
12
|
*
|
|
13
13
|
* RenderState — the owner object: RenderState.apply(runner, bag, env).
|
|
14
|
-
*
|
|
14
|
+
* RENDER_STATE_KEYS, RENDER_STATE_VERSION — the bag's key table and its one version.
|
|
15
|
+
* normalizeSavedRenderState(raw) — a bag or null → the full bag; THROWS on a key outside the table, another
|
|
16
|
+
* version, page controls, or a non-bag.
|
|
15
17
|
* pickCameraReplay(saved) — pure decision: named preset over raw orbit.
|
|
16
18
|
* partitionRenderRows(rows, declared, known)
|
|
17
19
|
* — resolve render rows against the TARGET renderer.
|
|
@@ -29,7 +31,7 @@
|
|
|
29
31
|
|
|
30
32
|
// The one owner of the renderState contract and its apply procedure.
|
|
31
33
|
import * as RenderState from './apply.js';
|
|
32
|
-
import { normalizeSavedRenderState, pickCameraReplay, partitionRenderRows, renderRowsFromBag } from './apply.js';
|
|
34
|
+
import { RENDER_STATE_KEYS, RENDER_STATE_VERSION, normalizeSavedRenderState, pickCameraReplay, partitionRenderRows, renderRowsFromBag } from './apply.js';
|
|
33
35
|
|
|
34
36
|
// ── applySavedUrlOverrides ───────────────────────────────────────────────────
|
|
35
37
|
|
|
@@ -65,6 +67,8 @@ function applySavedUrlOverrides(src, overrides) {
|
|
|
65
67
|
// ── Export ────────────────────────────────────────────────────────────────────
|
|
66
68
|
|
|
67
69
|
export {
|
|
70
|
+
RENDER_STATE_KEYS,
|
|
71
|
+
RENDER_STATE_VERSION,
|
|
68
72
|
RenderState,
|
|
69
73
|
normalizeSavedRenderState,
|
|
70
74
|
pickCameraReplay,
|
|
@@ -133,7 +133,7 @@ var _renderObj = {
|
|
|
133
133
|
* naming the global and the gate, rather than yielding undefined or null. A soft
|
|
134
134
|
* return is exactly what turns a boot-ordering defect into an opaque
|
|
135
135
|
* property-access TypeError one frame later ("Cannot read properties of null
|
|
136
|
-
* (reading 'build')"
|
|
136
|
+
* (reading 'build')"); the throw names what was unavailable instead.
|
|
137
137
|
* That covers the constructor members (`FrqtlApp`, `RecipeContext`) and every getter generated from
|
|
138
138
|
* the member table (CAPS_FRQTL_MEMBERS): there is no soft residual.
|
|
139
139
|
*
|
|
@@ -441,7 +441,7 @@ function _runMath(code, fileId, sourceUrl) {
|
|
|
441
441
|
* handle minted before the call is dead — a later use throws
|
|
442
442
|
* `arena:stale-handle`. For `js-reference` it is a no-op.
|
|
443
443
|
*
|
|
444
|
-
* THE SCOPE RULE
|
|
444
|
+
* THE SCOPE RULE: a math scope is one RUN, and the caller
|
|
445
445
|
* that knows what a run is owns the boundary. Two run units exist, each stated
|
|
446
446
|
* once: the `run()` lane's unit is one code buffer (it calls this in its own
|
|
447
447
|
* `finally`); a notebook's unit is one notebook run, so `NotebookKernel` calls
|
|
@@ -576,7 +576,7 @@ function _runMathModule(code, sdk, sourceUrl) {
|
|
|
576
576
|
* - _ensureMathSdk() (resolved SDK + backend select)
|
|
577
577
|
* - the stashed-console tee (the 7cc5520 lesson — observers never re-emit)
|
|
578
578
|
*
|
|
579
|
-
* It does NOT end the math scope: a cell is not a run
|
|
579
|
+
* It does NOT end the math scope: a cell is not a run. Its
|
|
580
580
|
* declared outputs are committed into the caller's binding store and must stay
|
|
581
581
|
* usable in later cells, so the scope boundary belongs to whoever owns that
|
|
582
582
|
* store — see `resetMathScope()`.
|
|
@@ -839,7 +839,7 @@ function runCell(spec) {
|
|
|
839
839
|
// NO math-scope reset here. A cell is not a run: the values this cell
|
|
840
840
|
// just returned are about to be committed into the caller's binding
|
|
841
841
|
// store, and draining the arena would make every one of them a stale
|
|
842
|
-
// handle before the next cell could read it
|
|
842
|
+
// handle before the next cell could read it. The
|
|
843
843
|
// scope boundary is the binding store's — see resetMathScope().
|
|
844
844
|
});
|
|
845
845
|
}).catch(function (e) {
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/* Copyright 2023-2026 by Essam Abadir */
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* lib/shim/storage/codec-chains.js — THE owner of a ROSETTA codec chain saved in browser storage.
|
|
5
|
+
*
|
|
6
|
+
* A codec chain is one module text (the codec page's `chainToModule` output) under a name. It is not a sketch
|
|
7
|
+
* project (frqtl.json is "kind": "scene"), so it has its own small store:
|
|
8
|
+
*
|
|
9
|
+
* CODEC_CHAINS_NAMESPACE = 'codec-chains'
|
|
10
|
+
* CODEC_CHAIN_KEYS = ['name', 'source'] the stored row, keyed by `name`
|
|
11
|
+
*
|
|
12
|
+
* saveChain(provider, row) → name Save: write under its own name (overwrites)
|
|
13
|
+
* saveChainAs(provider, row, newName) → newName Save As: REFUSED when newName is taken
|
|
14
|
+
* loadChain(provider, name) → row | null
|
|
15
|
+
* listChains(provider) → [{ name }] ascending
|
|
16
|
+
* deleteChain(provider, name)
|
|
17
|
+
*
|
|
18
|
+
* The name follows the storage seam's key rule (index.js checkStorageKey), as a project folder's does. The
|
|
19
|
+
* codec page's deep link `?module=<name>` is a key of this namespace.
|
|
20
|
+
*
|
|
21
|
+
* FAIL-LOUD. Layer 2 (physics): none.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
'use strict';
|
|
25
|
+
|
|
26
|
+
import { checkStorageKey } from './index.js';
|
|
27
|
+
|
|
28
|
+
export var CODEC_CHAINS_NAMESPACE = 'codec-chains';
|
|
29
|
+
export var CODEC_CHAIN_KEYS = Object.freeze(['name', 'source']);
|
|
30
|
+
|
|
31
|
+
function _fail(msg) {
|
|
32
|
+
throw new Error('[codec-chains] ' + msg + ' (FAIL-LOUD.)');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** @returns {Readonly<{name: string, source: string}>} */
|
|
36
|
+
export function normalizeChainRow(row) {
|
|
37
|
+
if (!row || typeof row !== 'object' || Array.isArray(row)) {
|
|
38
|
+
_fail('a codec chain row is { ' + CODEC_CHAIN_KEYS.join(', ') + ' } (got ' + (row === null ? 'null' : typeof row) + ').');
|
|
39
|
+
}
|
|
40
|
+
var unknown = Object.keys(row).filter(function (k) { return CODEC_CHAIN_KEYS.indexOf(k) === -1; });
|
|
41
|
+
if (unknown.length) {
|
|
42
|
+
_fail('unknown key ' + unknown.map(function (k) { return JSON.stringify(k); }).join(', ') +
|
|
43
|
+
' — a codec chain row holds exactly CODEC_CHAIN_KEYS: ' + CODEC_CHAIN_KEYS.join(', ') + '.');
|
|
44
|
+
}
|
|
45
|
+
checkStorageKey(row.name, 'a codec chain name');
|
|
46
|
+
if (typeof row.source !== 'string' || row.source.length === 0) {
|
|
47
|
+
_fail('"source" is the chain\'s module text, a non-empty string.');
|
|
48
|
+
}
|
|
49
|
+
return Object.freeze({ name: row.name, source: row.source });
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function saveChain(provider, row) {
|
|
53
|
+
var n = normalizeChainRow(row);
|
|
54
|
+
provider.put(CODEC_CHAINS_NAMESPACE, n.name, { name: n.name, source: n.source });
|
|
55
|
+
return n.name;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function saveChainAs(provider, row, newName) {
|
|
59
|
+
checkStorageKey(newName, 'a codec chain name');
|
|
60
|
+
var n = normalizeChainRow(Object.assign({}, row, { name: newName }));
|
|
61
|
+
if (provider.get(CODEC_CHAINS_NAMESPACE, newName) !== null) {
|
|
62
|
+
_fail('a codec chain named "' + newName + '" is already saved — Save As takes a name no saved chain has.');
|
|
63
|
+
}
|
|
64
|
+
provider.put(CODEC_CHAINS_NAMESPACE, n.name, { name: n.name, source: n.source });
|
|
65
|
+
return n.name;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function loadChain(provider, name) {
|
|
69
|
+
checkStorageKey(name, 'a codec chain name');
|
|
70
|
+
var raw = provider.get(CODEC_CHAINS_NAMESPACE, name);
|
|
71
|
+
if (raw === null || raw === undefined) { return null; }
|
|
72
|
+
var row;
|
|
73
|
+
try { row = normalizeChainRow(raw); }
|
|
74
|
+
catch (e) { _fail('the saved chain "' + name + '" is not a codec chain row: ' + ((e && e.message) || e)); }
|
|
75
|
+
if (row.name !== name) { _fail('the saved chain under "' + name + '" names itself "' + row.name + '".'); }
|
|
76
|
+
return row;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function listChains(provider) {
|
|
80
|
+
return provider.list(CODEC_CHAINS_NAMESPACE).map(function (e) { return e.key; }).sort().map(function (k) {
|
|
81
|
+
return { name: k };
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function deleteChain(provider, name) {
|
|
86
|
+
checkStorageKey(name, 'a codec chain name');
|
|
87
|
+
provider['delete'](CODEC_CHAINS_NAMESPACE, name);
|
|
88
|
+
}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/* Copyright 2023-2026 by Essam Abadir */
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* lib/shim/storage/folders.js — THE owner of a project saved in browser storage: one project = one folder.
|
|
5
|
+
*
|
|
6
|
+
* A project is a folder of files: `frqtl.json` (its owner is
|
|
7
|
+
* lib/shim/project/manifest.js), the entry sketch it names, and whatever else the author keeps beside them.
|
|
8
|
+
* The stored ROW is the folder plus the host's boot bag, and nothing else:
|
|
9
|
+
*
|
|
10
|
+
* FOLDER_ROW_KEYS = ['files', 'renderState']
|
|
11
|
+
* files { path: text } — text only (a binary value is refused by name); every path through
|
|
12
|
+
* lib/shim/project/identity.js checkProjectPath, the one path rule; a root `index.html` is
|
|
13
|
+
* refused (EXPORT_PAGE: the Export writes it); `frqtl.json` is required and read by its owner.
|
|
14
|
+
* renderState the host's boot bag (STUDIO's capture, a rung's effectiveRenderState), normalized by its one
|
|
15
|
+
* owner (lib/shim/render-state normalizeSavedRenderState) and ferried — never re-listed here.
|
|
16
|
+
* Boot facts live beside the folder, never in frqtl.json.
|
|
17
|
+
*
|
|
18
|
+
* The key is the manifest `name`, in FOLDERS_NAMESPACE. Exports:
|
|
19
|
+
*
|
|
20
|
+
* normalizeFolderRow(row) → the frozen row (the one validation every reader and writer uses)
|
|
21
|
+
* folderNameOf(row) → its frqtl.json name
|
|
22
|
+
* saveFolder(provider, row) → name Save: write the folder under its own name (overwrites)
|
|
23
|
+
* saveFolderAs(provider, row, newName) → newName Save As: frqtl.json's name rewritten through
|
|
24
|
+
* serializeManifest; REFUSED when newName is taken
|
|
25
|
+
* copyFolder(provider, name, newName) → newName a stored folder copied (saveFolderAs over loadFolder)
|
|
26
|
+
* loadFolder(provider, name) → row | null
|
|
27
|
+
* listFolders(provider) → [{ name }] in ascending name order
|
|
28
|
+
* deleteFolder(provider, name)
|
|
29
|
+
*
|
|
30
|
+
* Retired saves (DESCIX IS NOT LIVE; there is no migration tool): the namespaces RETIRED_NAMESPACES (owned by
|
|
31
|
+
* the storage seam, index.js, which refuses a write into one) held the v4 module envelope and the design
|
|
32
|
+
* envelope. They are never read and never converted:
|
|
33
|
+
*
|
|
34
|
+
* retiredSaves(provider) → [{ namespace, key }] — every key in a retired namespace
|
|
35
|
+
* deleteRetiredSaves(provider, saves) → the count deleted — EXACTLY the listed keys (each must be in a
|
|
36
|
+
* retired namespace); nothing else is touched
|
|
37
|
+
*
|
|
38
|
+
* FAIL-LOUD: every refusal names the rule it applies. Layer 2 (physics): none — this stores text.
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
'use strict';
|
|
42
|
+
|
|
43
|
+
import { RETIRED_NAMESPACES, checkStorageKey } from './index.js';
|
|
44
|
+
import { checkProjectPath } from '../project/identity.js';
|
|
45
|
+
import { MANIFEST_FILE, parseManifest, serializeManifest } from '../project/manifest.js';
|
|
46
|
+
import { normalizeSavedRenderState } from '../render-state/index.js';
|
|
47
|
+
|
|
48
|
+
export { RETIRED_NAMESPACES };
|
|
49
|
+
|
|
50
|
+
/** The storage namespace every project folder is saved under. */
|
|
51
|
+
export var FOLDERS_NAMESPACE = 'project-folders';
|
|
52
|
+
|
|
53
|
+
/** The stored row's keys — the folder and the host's boot bag. */
|
|
54
|
+
export var FOLDER_ROW_KEYS = Object.freeze(['files', 'renderState']);
|
|
55
|
+
|
|
56
|
+
/** The root page the Export generates (lib/shim/bundle) — reserved, so a folder never holds one. */
|
|
57
|
+
export var EXPORT_PAGE = 'index.html';
|
|
58
|
+
|
|
59
|
+
function _fail(msg) {
|
|
60
|
+
throw new Error('[folders] ' + msg + ' (FAIL-LOUD.)');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function _isPlainObject(v) { return !!v && typeof v === 'object' && !Array.isArray(v); }
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* @param {{files: Object<string,string>, renderState?: object|null}} row
|
|
67
|
+
* @returns {Readonly<{files: Readonly<Object<string,string>>, renderState: object}>}
|
|
68
|
+
*/
|
|
69
|
+
export function normalizeFolderRow(row) {
|
|
70
|
+
if (!_isPlainObject(row)) {
|
|
71
|
+
_fail('a project folder row is { ' + FOLDER_ROW_KEYS.join(', ') + ' } (got ' +
|
|
72
|
+
(row === null ? 'null' : Array.isArray(row) ? 'an array' : typeof row) + ').');
|
|
73
|
+
}
|
|
74
|
+
var unknown = Object.keys(row).filter(function (k) { return FOLDER_ROW_KEYS.indexOf(k) === -1; });
|
|
75
|
+
if (unknown.length) {
|
|
76
|
+
_fail('unknown key ' + unknown.map(function (k) { return JSON.stringify(k); }).join(', ') +
|
|
77
|
+
' — a project folder row holds exactly FOLDER_ROW_KEYS (lib/shim/storage/folders.js): ' +
|
|
78
|
+
FOLDER_ROW_KEYS.join(', ') + '.');
|
|
79
|
+
}
|
|
80
|
+
var files = row.files;
|
|
81
|
+
if (!_isPlainObject(files) || Object.keys(files).length === 0) {
|
|
82
|
+
_fail('"files" is the folder { path: text } with at least ' + MANIFEST_FILE + ' (got ' +
|
|
83
|
+
(_isPlainObject(files) ? 'an empty map' : typeof files) + ').');
|
|
84
|
+
}
|
|
85
|
+
var out = {};
|
|
86
|
+
Object.keys(files).forEach(function (p) {
|
|
87
|
+
checkProjectPath(p);
|
|
88
|
+
if (p === EXPORT_PAGE) {
|
|
89
|
+
_fail('"' + EXPORT_PAGE + '" at the folder root is reserved — the Export generates it (the host page ' +
|
|
90
|
+
'that carries the boot bag); a project folder never holds one.');
|
|
91
|
+
}
|
|
92
|
+
if (typeof files[p] !== 'string') {
|
|
93
|
+
_fail('"' + p + '" is not text (got ' + (files[p] instanceof Uint8Array ? 'bytes' : typeof files[p]) +
|
|
94
|
+
') — a project folder holds text files only.');
|
|
95
|
+
}
|
|
96
|
+
out[p] = files[p];
|
|
97
|
+
});
|
|
98
|
+
if (typeof out[MANIFEST_FILE] !== 'string') {
|
|
99
|
+
_fail('the folder carries no ' + MANIFEST_FILE + ' — a project folder is named and entered by its manifest ' +
|
|
100
|
+
'(lib/shim/project/manifest.js manifestFor({ name, entry }) writes one).');
|
|
101
|
+
}
|
|
102
|
+
var name = parseManifest(out[MANIFEST_FILE]).name;
|
|
103
|
+
checkStorageKey(name, 'a project folder name (frqtl.json "name")');
|
|
104
|
+
return Object.freeze({ files: Object.freeze(out), renderState: normalizeSavedRenderState(row.renderState) });
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** @param {object} row @returns {string} the folder's frqtl.json name */
|
|
108
|
+
export function folderNameOf(row) {
|
|
109
|
+
return parseManifest(normalizeFolderRow(row).files[MANIFEST_FILE]).name;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function _stored(row) {
|
|
113
|
+
return { files: Object.assign({}, row.files), renderState: row.renderState };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Save: write the folder under its own frqtl.json name, overwriting that name's folder.
|
|
118
|
+
* @returns {string} the name
|
|
119
|
+
*/
|
|
120
|
+
export function saveFolder(provider, row) {
|
|
121
|
+
var n = normalizeFolderRow(row);
|
|
122
|
+
var name = parseManifest(n.files[MANIFEST_FILE]).name;
|
|
123
|
+
provider.put(FOLDERS_NAMESPACE, name, _stored(n));
|
|
124
|
+
return name;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Save As: the folder under `newName` — frqtl.json's name rewritten through serializeManifest (the one writer).
|
|
129
|
+
* REFUSED when a folder named `newName` is already saved.
|
|
130
|
+
* @returns {string} newName
|
|
131
|
+
*/
|
|
132
|
+
export function saveFolderAs(provider, row, newName) {
|
|
133
|
+
checkStorageKey(newName, 'a project folder name');
|
|
134
|
+
var n = normalizeFolderRow(row);
|
|
135
|
+
if (provider.get(FOLDERS_NAMESPACE, newName) !== null) {
|
|
136
|
+
_fail('a project named "' + newName + '" is already saved — Save As takes a name no saved project has ' +
|
|
137
|
+
'(choose another, or delete the saved one first).');
|
|
138
|
+
}
|
|
139
|
+
var files = Object.assign({}, n.files);
|
|
140
|
+
files[MANIFEST_FILE] = serializeManifest(Object.assign({}, parseManifest(n.files[MANIFEST_FILE]), { name: newName }));
|
|
141
|
+
var renamed = normalizeFolderRow({ files: files, renderState: n.renderState });
|
|
142
|
+
provider.put(FOLDERS_NAMESPACE, newName, _stored(renamed));
|
|
143
|
+
return newName;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** A stored folder copied under `newName` (frqtl.json's name rewritten); refuses a taken name or a missing source. */
|
|
147
|
+
export function copyFolder(provider, name, newName) {
|
|
148
|
+
var row = loadFolder(provider, name);
|
|
149
|
+
if (!row) { _fail('no saved project named "' + name + '" to copy.'); }
|
|
150
|
+
return saveFolderAs(provider, row, newName);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** @returns {Readonly<object>|null} the normalized row, or null when no folder has that name */
|
|
154
|
+
export function loadFolder(provider, name) {
|
|
155
|
+
checkStorageKey(name, 'a project folder name');
|
|
156
|
+
var raw = provider.get(FOLDERS_NAMESPACE, name);
|
|
157
|
+
if (raw === null || raw === undefined) { return null; }
|
|
158
|
+
var row;
|
|
159
|
+
try { row = normalizeFolderRow(raw); }
|
|
160
|
+
catch (e) { _fail('the saved project "' + name + '" is not a project folder row: ' + ((e && e.message) || e)); }
|
|
161
|
+
var stored = parseManifest(row.files[MANIFEST_FILE]).name;
|
|
162
|
+
if (stored !== name) {
|
|
163
|
+
_fail('the saved project under "' + name + '" names itself "' + stored + '" in ' + MANIFEST_FILE +
|
|
164
|
+
' — a folder is keyed by its manifest name.');
|
|
165
|
+
}
|
|
166
|
+
return row;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** @returns {Array<{name: string}>} every saved folder, ascending by name */
|
|
170
|
+
export function listFolders(provider) {
|
|
171
|
+
return provider.list(FOLDERS_NAMESPACE).map(function (e) { return e.key; }).sort().map(function (k) {
|
|
172
|
+
return { name: k };
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export function deleteFolder(provider, name) {
|
|
177
|
+
checkStorageKey(name, 'a project folder name');
|
|
178
|
+
provider['delete'](FOLDERS_NAMESPACE, name);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** @returns {Array<{namespace: string, key: string}>} every key in a retired namespace (never read) */
|
|
182
|
+
export function retiredSaves(provider) {
|
|
183
|
+
var out = [];
|
|
184
|
+
RETIRED_NAMESPACES.forEach(function (ns) {
|
|
185
|
+
provider.list(ns).forEach(function (e) { out.push({ namespace: ns, key: e.key }); });
|
|
186
|
+
});
|
|
187
|
+
return out;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Delete exactly `saves` (retiredSaves' rows); a row outside a retired namespace is refused before anything
|
|
192
|
+
* is deleted.
|
|
193
|
+
* @returns {number} the count deleted
|
|
194
|
+
*/
|
|
195
|
+
export function deleteRetiredSaves(provider, saves) {
|
|
196
|
+
if (!Array.isArray(saves)) { _fail('deleteRetiredSaves(provider, saves): saves is retiredSaves(provider)\'s list.'); }
|
|
197
|
+
saves.forEach(function (s) {
|
|
198
|
+
if (!s || RETIRED_NAMESPACES.indexOf(s.namespace) === -1 || typeof s.key !== 'string') {
|
|
199
|
+
_fail('deleteRetiredSaves deletes only retired saves ({ namespace in [' + RETIRED_NAMESPACES.join(', ') +
|
|
200
|
+
'], key }) — got ' + JSON.stringify(s) + '.');
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
saves.forEach(function (s) { provider['delete'](s.namespace, s.key); });
|
|
204
|
+
return saves.length;
|
|
205
|
+
}
|
|
@@ -45,16 +45,27 @@
|
|
|
45
45
|
* // Then anywhere in the IDE:
|
|
46
46
|
* import { getProvider } from 'lib/shim/storage';
|
|
47
47
|
* const storage = getProvider();
|
|
48
|
-
* storage.put('
|
|
49
|
-
* const saved = storage.get('
|
|
50
|
-
* const all = storage.list('
|
|
51
|
-
* storage.delete('
|
|
48
|
+
* storage.put('explainers', 'gym', { seen: true });
|
|
49
|
+
* const saved = storage.get('explainers', 'gym');
|
|
50
|
+
* const all = storage.list('explainers');
|
|
51
|
+
* storage.delete('explainers', 'gym');
|
|
52
52
|
*
|
|
53
53
|
* ── Contract validation ─────────────────────────────────────────────────────
|
|
54
54
|
*
|
|
55
55
|
* validateProvider(provider) — throws if provider does not implement the
|
|
56
56
|
* contract. Called by setActiveProvider; consumers may also call it.
|
|
57
57
|
*
|
|
58
|
+
* ── Retired namespaces ──────────────────────────────────────────────────────
|
|
59
|
+
*
|
|
60
|
+
* RETIRED_NAMESPACES — the namespaces of the retired save formats ('files': the v4 module
|
|
61
|
+
* envelope STUDIO and ROSETTA wrote; 'projects': the opaque design envelope). The project
|
|
62
|
+
* folder (folders.js) and the codec chain (codec-chains.js) replaced them. The active provider
|
|
63
|
+
* (getProvider) REFUSES a write into one, naming the replacement; reads, lists and deletes go
|
|
64
|
+
* through, so the retired-saves strip can name them and delete exactly them (folders.js
|
|
65
|
+
* retiredSaves / deleteRetiredSaves). Nothing reads or converts their rows.
|
|
66
|
+
* checkStorageKey(key, what) — THE key rule of a store keyed by a name (a project folder's
|
|
67
|
+
* frqtl.json name, a codec chain's name): a non-empty string with no "/" (reserved).
|
|
68
|
+
*
|
|
58
69
|
* ── Server-side seam ───────────────────────────────────────────────────────
|
|
59
70
|
*
|
|
60
71
|
* The matching server routes are defined in lib/server/mounts.js under
|
|
@@ -69,6 +80,49 @@
|
|
|
69
80
|
|
|
70
81
|
/** @type {StorageProvider|null} */
|
|
71
82
|
var _activeProvider = null;
|
|
83
|
+
/** @type {StorageProvider|null} the active provider behind the retired-namespace write guard */
|
|
84
|
+
var _activeGuard = null;
|
|
85
|
+
|
|
86
|
+
/** The namespaces of the retired save formats — written by nothing (see the header). */
|
|
87
|
+
export var RETIRED_NAMESPACES = Object.freeze(['files', 'projects']);
|
|
88
|
+
|
|
89
|
+
var _RETIRED_BY = Object.freeze({
|
|
90
|
+
files: 'the project folder (lib/shim/storage/folders.js, namespace "project-folders") for a STUDIO save, ' +
|
|
91
|
+
'the codec chain (lib/shim/storage/codec-chains.js, namespace "codec-chains") for a ROSETTA save',
|
|
92
|
+
projects: 'the project folder (lib/shim/storage/folders.js, namespace "project-folders")'
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* THE key rule of a store keyed by a name: a non-empty string with no "/" (reserved by the contract).
|
|
97
|
+
* @param {*} key
|
|
98
|
+
* @param {string} what — the store's noun, for the message (e.g. 'a project folder name')
|
|
99
|
+
* @throws {Error}
|
|
100
|
+
*/
|
|
101
|
+
export function checkStorageKey(key, what) {
|
|
102
|
+
if (typeof key !== 'string' || key.length === 0) {
|
|
103
|
+
throw new Error('[StorageProvider] ' + what + ' is a non-empty string (got ' + JSON.stringify(key) + '). (FAIL-LOUD.)');
|
|
104
|
+
}
|
|
105
|
+
if (key.indexOf('/') !== -1) {
|
|
106
|
+
throw new Error('[StorageProvider] ' + what + ' ' + JSON.stringify(key) + ' contains "/" — "/" is reserved ' +
|
|
107
|
+
'in a storage key; name it without one. (FAIL-LOUD.)');
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function _guarded(provider) {
|
|
112
|
+
return Object.freeze({
|
|
113
|
+
get: function (namespace, key) { return provider.get(namespace, key); },
|
|
114
|
+
put: function (namespace, key, value) {
|
|
115
|
+
if (RETIRED_NAMESPACES.indexOf(namespace) !== -1) {
|
|
116
|
+
throw new Error('[StorageProvider] a write into the retired namespace "' + namespace + '" (key ' +
|
|
117
|
+
JSON.stringify(key) + ') is refused — that save format is retired; ' + _RETIRED_BY[namespace] +
|
|
118
|
+
' replaced it. (FAIL-LOUD.)');
|
|
119
|
+
}
|
|
120
|
+
return provider.put(namespace, key, value);
|
|
121
|
+
},
|
|
122
|
+
'delete': function (namespace, key) { return provider['delete'](namespace, key); },
|
|
123
|
+
list: function (namespace) { return provider.list(namespace); }
|
|
124
|
+
});
|
|
125
|
+
}
|
|
72
126
|
|
|
73
127
|
// ── Contract typedefs ──────────────────────────────────────────────────────
|
|
74
128
|
|
|
@@ -129,10 +183,11 @@ export function validateProvider(provider) {
|
|
|
129
183
|
export function setActiveProvider(provider) {
|
|
130
184
|
validateProvider(provider);
|
|
131
185
|
_activeProvider = provider;
|
|
186
|
+
_activeGuard = _guarded(provider);
|
|
132
187
|
}
|
|
133
188
|
|
|
134
189
|
/**
|
|
135
|
-
* Retrieve the active storage driver.
|
|
190
|
+
* Retrieve the active storage driver, behind the retired-namespace write guard (RETIRED_NAMESPACES).
|
|
136
191
|
* FAIL-LOUD: throws if no provider has been registered.
|
|
137
192
|
*
|
|
138
193
|
* @returns {StorageProvider}
|
|
@@ -146,18 +201,18 @@ export function getProvider() {
|
|
|
146
201
|
'for a non-browser driver.'
|
|
147
202
|
);
|
|
148
203
|
}
|
|
149
|
-
return
|
|
204
|
+
return _activeGuard;
|
|
150
205
|
}
|
|
151
206
|
|
|
152
207
|
/**
|
|
153
|
-
* Return the active provider, or null if none has been registered.
|
|
208
|
+
* Return the active provider (behind the same guard), or null if none has been registered.
|
|
154
209
|
* Use this instead of getProvider() when a nil-safe check is appropriate
|
|
155
210
|
* (e.g. during test setup before a provider is wired).
|
|
156
211
|
*
|
|
157
212
|
* @returns {StorageProvider|null}
|
|
158
213
|
*/
|
|
159
214
|
export function peekProvider() {
|
|
160
|
-
return
|
|
215
|
+
return _activeGuard;
|
|
161
216
|
}
|
|
162
217
|
|
|
163
218
|
/**
|
|
@@ -166,4 +221,5 @@ export function peekProvider() {
|
|
|
166
221
|
*/
|
|
167
222
|
export function resetProvider() {
|
|
168
223
|
_activeProvider = null;
|
|
224
|
+
_activeGuard = null;
|
|
169
225
|
}
|
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* lib/shim/storage/make-namespace-store.js — Generic namespace-CRUD factory.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* The id-keyed record store lib/shim/notebook/store.js is a thin config call
|
|
7
|
+
* over. (A project folder and a codec chain are keyed by NAME and have their
|
|
8
|
+
* own owners: folders.js, codec-chains.js.)
|
|
9
9
|
*
|
|
10
10
|
* ── Signature ──────────────────────────────────────────────────────────────
|
|
11
11
|
*
|
|
@@ -13,12 +13,12 @@
|
|
|
13
13
|
*
|
|
14
14
|
* ── Config ─────────────────────────────────────────────────────────────────
|
|
15
15
|
*
|
|
16
|
-
* namespace {string} — StorageProvider namespace key (e.g. '
|
|
16
|
+
* namespace {string} — StorageProvider namespace key (e.g. 'notebooks').
|
|
17
17
|
* schemaVersion {number} — Current schema version integer (stamped on save
|
|
18
18
|
* by callers; used here only in the returned
|
|
19
19
|
* constant — actual stamping is the caller's job,
|
|
20
20
|
* as in the originals).
|
|
21
|
-
* idPrefix {string} — Prefix for generated ids (e.g. '
|
|
21
|
+
* idPrefix {string} — Prefix for generated ids (e.g. 'nb_').
|
|
22
22
|
* validate {function} — validate(record): throws on invalid envelope.
|
|
23
23
|
* summaryFields {function} — summaryFields(record) → summary object, or
|
|
24
24
|
* returns null to silently skip the record.
|