@descix/frqtl-node 0.6.0-p1.1 → 0.6.0-p4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +1 -1
- package/README.md +3 -3
- package/dist/dist-manifest.json +27 -25
- package/dist/eyes/index.js +5 -2
- package/dist/frqtl/objects/FrqtlApp.js +28 -1
- package/dist/frqtl/objects/RecipeContext.js +178 -12
- package/dist/frqtl/objects/instruments/hosted-eyes.js +200 -29
- package/dist/frqtl/objects/record/Record.js +326 -0
- package/dist/frqtl/objects/record/open-record.js +22 -0
- package/dist/frqtl/parts/FrqtlVision.js +21 -6
- package/dist/shim/bundle/index.js +112 -96
- package/dist/shim/bundle/indexHtml.js +66 -39
- package/dist/shim/engine/router.js +2 -2
- package/dist/shim/index.js +1 -1
- package/dist/shim/notebook/format.js +1 -1
- package/dist/shim/notebook/store.js +2 -3
- package/dist/shim/project/index.js +16 -3
- package/dist/shim/project/link.js +181 -43
- package/dist/shim/project/manifest.js +151 -0
- package/dist/shim/project/parts-writer.js +9 -5
- package/dist/shim/project/record-format.js +345 -0
- package/dist/shim/project/specifiers.js +1 -1
- package/dist/shim/registry/index.js +1 -1
- package/dist/shim/registry/scene-source.js +44 -30
- package/dist/shim/registry/walkthrough.js +4 -3
- package/dist/shim/storage/codec-chains.js +88 -0
- package/dist/shim/storage/folders.js +205 -0
- package/dist/shim/storage/index.js +64 -8
- package/dist/shim/storage/make-namespace-store.js +5 -5
- package/llms.txt +2 -2
- package/package.json +1 -1
- package/dist/shim/bundle/frqnb.js +0 -112
- package/dist/shim/bundle/manifest.js +0 -95
- package/dist/shim/storage/modules.js +0 -249
- package/dist/shim/storage/projects.js +0 -150
|
@@ -69,7 +69,8 @@
|
|
|
69
69
|
* project whose entry's default export is `buildExperiment(exp)` (a one-string sketch, such as a
|
|
70
70
|
* shipped scene file, is `{ files: { 'sketch.js': text }, entry: 'sketch.js' }`). The
|
|
71
71
|
* page links it through its one door and stamps the project's identity (projectIdentity) as the
|
|
72
|
-
* record's `sourceSha256`; this process computes the same identity from the same
|
|
72
|
+
* record's `sourceSha256`; this process computes the same identity from the same linked set
|
|
73
|
+
* (lib/shim/project/link.js linkedFiles — frqtl.json and the module files; a README rides unhashed), and the
|
|
73
74
|
* open-time record check holds the two equal. A caller without one is REPLAY-grade
|
|
74
75
|
* (an ad-hoc tick-fn scene, a live-edited scene, a bare raster row) and is REFUSED in
|
|
75
76
|
* RENDERER_CONTRACT §10.3's own vocabulary: there is nothing to regenerate FROM, and a
|
|
@@ -132,6 +133,7 @@ import { FrqtlCaptureHost } from './capture-host.js';
|
|
|
132
133
|
import { FrqtlVision } from '../../parts/FrqtlVision.js';
|
|
133
134
|
import { FrqtlCapture } from './capture.js';
|
|
134
135
|
import { projectIdentity, projectBytes } from '../../../shim/project/identity.js';
|
|
136
|
+
import { linkedFiles, entryOf } from '../../../shim/project/link.js';
|
|
135
137
|
import { libPathOf } from '../../../shim/project/specifiers.js';
|
|
136
138
|
|
|
137
139
|
|
|
@@ -191,7 +193,8 @@ import { libPathOf } from '../../../shim/project/specifiers.js';
|
|
|
191
193
|
// ── The REGENERATE gate ───────────────────────────────────────────────────
|
|
192
194
|
|
|
193
195
|
/**
|
|
194
|
-
* assertHostable({sceneRecord, sceneSource}) — throw-or-return, in §10.3's vocabulary.
|
|
196
|
+
* assertHostable({sceneRecord, sceneSource}) — throw-or-return the entry, in §10.3's vocabulary. The entry is
|
|
197
|
+
* THE entry rule's answer (link.js entryOf): the entry frqtl.json names, else sceneSource.entry.
|
|
195
198
|
* @private
|
|
196
199
|
*/
|
|
197
200
|
function _assertHostable(opts) {
|
|
@@ -213,18 +216,20 @@ import { libPathOf } from '../../../shim/project/specifiers.js';
|
|
|
213
216
|
'(more files beside it for a project with forked parts). (FAIL-LOUD.)');
|
|
214
217
|
}
|
|
215
218
|
if (!src || typeof src !== 'object' || src.kind !== 'project' ||
|
|
216
|
-
!src.files || typeof src.files !== 'object' || Object.keys(src.files).length === 0
|
|
217
|
-
typeof src.entry !== 'string' || !src.entry) {
|
|
219
|
+
!src.files || typeof src.files !== 'object' || Object.keys(src.files).length === 0) {
|
|
218
220
|
throw new Error('[FrqtlHostedEyes.open] this scene is REPLAY-grade, not ' +
|
|
219
221
|
'REGENERATE-grade: hosting re-RUNS the scene in another process, so a ' +
|
|
220
222
|
'SceneRecord alone is not enough (it is identity + reproduction pins, with no ' +
|
|
221
|
-
'scene spec in it). Pass the pair — sceneSource: { kind: "project", files, entry }
|
|
223
|
+
'scene spec in it). Pass the pair — sceneSource: { kind: "project", files, entry? } (entry optional when ' +
|
|
224
|
+
'files carries the frqtl.json that names it) — ' +
|
|
222
225
|
'where the entry\'s default export is buildExperiment(exp) and the canonical empty ' +
|
|
223
226
|
'page links it. An ad-hoc tick-fn scene, a live-edited scene or a ' +
|
|
224
227
|
'bare raster row has nothing to regenerate FROM, and a silently approximate ' +
|
|
225
228
|
're-run is the one failure a content-addressed corpus cannot detect afterwards. ' +
|
|
226
229
|
'(FAIL-LOUD — see RENDERER_CONTRACT.md §10.3.)');
|
|
227
230
|
}
|
|
231
|
+
// THE entry rule (link.js entryOf): frqtl.json's entry, a given one equal to it, else the given one.
|
|
232
|
+
return entryOf(src.files, src.entry);
|
|
228
233
|
}
|
|
229
234
|
|
|
230
235
|
/**
|
|
@@ -478,16 +483,10 @@ import { libPathOf } from '../../../shim/project/specifiers.js';
|
|
|
478
483
|
}
|
|
479
484
|
applied = window.FrqtlPage.applyRenderState(args.renderState);
|
|
480
485
|
}
|
|
481
|
-
// ESTABLISH THE CLOCK with one step, deliberately
|
|
482
|
-
//
|
|
483
|
-
//
|
|
484
|
-
//
|
|
485
|
-
// read as "tick 10"). Every later read here is exact BECAUSE this process is the only
|
|
486
|
-
// stepper (the RAF loop is stopped above), so the fix is to make the first reading a
|
|
487
|
-
// real one rather than to trust a cache the re-init left behind. It costs nothing the
|
|
488
|
-
// contract was not already charging: a capture before the first tick is REFUSED
|
|
489
|
-
// anyway ("no tick has run yet" — never an empty buffer), so tick 1 is the earliest
|
|
490
|
-
// capturable coordinate of any scene.
|
|
486
|
+
// ESTABLISH THE CLOCK with one step, deliberately: applying a recipe begins a world that has not stepped
|
|
487
|
+
// (runner.worldTick 0, no TickData yet), and a capture before the first tick is REFUSED ("no tick has run yet"
|
|
488
|
+
// — never an empty buffer), so tick 1 is the earliest capturable coordinate of any scene. Every later read is
|
|
489
|
+
// exact because this process is the only stepper (the RAF loop is stopped above).
|
|
491
490
|
r.step();
|
|
492
491
|
return { renderState: applied };
|
|
493
492
|
};
|
|
@@ -553,8 +552,8 @@ import { libPathOf } from '../../../shim/project/specifiers.js';
|
|
|
553
552
|
return;
|
|
554
553
|
}
|
|
555
554
|
try {
|
|
556
|
-
|
|
557
|
-
resolve(
|
|
555
|
+
r.step();
|
|
556
|
+
resolve(r.worldTick);
|
|
558
557
|
} catch (e) { reject(e); }
|
|
559
558
|
})();
|
|
560
559
|
});
|
|
@@ -562,8 +561,9 @@ import { libPathOf } from '../../../shim/project/specifiers.js';
|
|
|
562
561
|
|
|
563
562
|
/** The current tick, read (never advanced). */
|
|
564
563
|
var _IN_PAGE_TICK = function () {
|
|
565
|
-
var
|
|
566
|
-
|
|
564
|
+
var r = window.FrqtlPage.runner;
|
|
565
|
+
if (!r) { throw new Error('[FrqtlHostedEyes/in-page] there is no runner to read a tick from (FrqtlPage.runner). (FAIL-LOUD.)'); }
|
|
566
|
+
return r.worldTick;
|
|
567
567
|
};
|
|
568
568
|
|
|
569
569
|
/**
|
|
@@ -606,6 +606,99 @@ import { libPathOf } from '../../../shim/project/specifiers.js';
|
|
|
606
606
|
return cam.getOrbit();
|
|
607
607
|
};
|
|
608
608
|
|
|
609
|
+
// ── The Record door, in the page (lib/frqtl/objects/record/Record.js through FrqtlPage.experiment) ──
|
|
610
|
+
// The page's facade owns the Record; these routines only forward to it, so the folder a node host gets
|
|
611
|
+
// is the one the page's own door wrote (one implementation, every host). The open Record and a film
|
|
612
|
+
// in flight are parked on the page under ONE name, `__frqtlHostedRecord`.
|
|
613
|
+
|
|
614
|
+
/** exp.startRecord() on the page's facade; parks the Record. */
|
|
615
|
+
var _IN_PAGE_RECORD_START = async function () {
|
|
616
|
+
var exp = window.FrqtlPage.experiment;
|
|
617
|
+
if (!exp || typeof exp.startRecord !== 'function') {
|
|
618
|
+
throw new Error('[FrqtlHostedEyes/in-page] this page has no FrqtlPage.experiment facade to record.');
|
|
619
|
+
}
|
|
620
|
+
var rec;
|
|
621
|
+
// A refusal crosses the page boundary as data, so its err.record reaches node (page.evaluate keeps only a message).
|
|
622
|
+
try { rec = await exp.startRecord(); }
|
|
623
|
+
catch (e) { if (e && e.record) { return { refused: { message: e.message, record: e.record } }; } throw e; }
|
|
624
|
+
window.__frqtlHostedRecord = { rec: rec, film: null };
|
|
625
|
+
return { tick: rec.tick };
|
|
626
|
+
};
|
|
627
|
+
|
|
628
|
+
/** rec.film(opts) begins: its first segment subscribes before this returns (the drive gate waits on it). */
|
|
629
|
+
var _IN_PAGE_RECORD_FILM_BEGIN = function (opts) {
|
|
630
|
+
var p = window.__frqtlHostedRecord;
|
|
631
|
+
if (!p || !p.rec) { throw new Error('[FrqtlHostedEyes/in-page] no Record is open — eyes.record.start() first.'); }
|
|
632
|
+
if (p.film) { throw new Error('[FrqtlHostedEyes/in-page] a film is already running — await it first.'); }
|
|
633
|
+
p.film = p.rec.film(opts);
|
|
634
|
+
p.film.then(null, function () {});
|
|
635
|
+
return true;
|
|
636
|
+
};
|
|
637
|
+
|
|
638
|
+
/** The film's clip spans, when it ends. */
|
|
639
|
+
var _IN_PAGE_RECORD_FILM_END = function () {
|
|
640
|
+
var p = window.__frqtlHostedRecord;
|
|
641
|
+
var f = p && p.film;
|
|
642
|
+
if (!f) { throw new Error('[FrqtlHostedEyes/in-page] no film is running.'); }
|
|
643
|
+
return f.then(function (spans) { p.film = null; return spans; }, function (e) { p.film = null; throw e; });
|
|
644
|
+
};
|
|
645
|
+
|
|
646
|
+
/** rec.stop() → the folder, each clip's bytes as base64 (the one encoder, capture-envelope.js). */
|
|
647
|
+
var _IN_PAGE_RECORD_STOP = async function (args) {
|
|
648
|
+
var p = window.__frqtlHostedRecord;
|
|
649
|
+
if (!p || !p.rec) { throw new Error('[FrqtlHostedEyes/in-page] no Record is open — eyes.record.start() first.'); }
|
|
650
|
+
var CE = await import(args.modules.captureEnvelope);
|
|
651
|
+
var out;
|
|
652
|
+
// A refused stop() leaves the Record open (still parked); its err.record crosses as data.
|
|
653
|
+
try { out = await p.rec.stop(); }
|
|
654
|
+
catch (e) { if (e && e.record) { return { refused: { message: e.message, record: e.record } }; } throw e; }
|
|
655
|
+
window.__frqtlHostedRecord = null;
|
|
656
|
+
if (out.empty) { return { empty: true }; }
|
|
657
|
+
var files = {};
|
|
658
|
+
Object.keys(out.files).forEach(function (k) {
|
|
659
|
+
var v = out.files[k];
|
|
660
|
+
files[k] = typeof v === 'string' ? { text: v } : { base64: CE.bytesToBase64(v) };
|
|
661
|
+
});
|
|
662
|
+
return { files: files, record: out.record, clips: out.clips };
|
|
663
|
+
};
|
|
664
|
+
|
|
665
|
+
/** One act through the page's own doors: toSketch({by}), setDeclared(name, patch, {by}), placeDeclaredAt(name, point). */
|
|
666
|
+
var _IN_PAGE_ACT = async function (args) {
|
|
667
|
+
var exp = window.FrqtlPage.experiment;
|
|
668
|
+
var r = window.FrqtlPage.runner;
|
|
669
|
+
if (args.door === 'toSketch') { return exp.toSketch(args.opts); }
|
|
670
|
+
if (args.door === 'setDeclared') { return exp.setDeclared(args.name, args.patch, args.opts); }
|
|
671
|
+
if (args.door === 'placeDeclaredAt') {
|
|
672
|
+
var d = r.declared.find(function (o) { return o.name === args.name; });
|
|
673
|
+
if (!d) { throw new Error('[FrqtlHostedEyes/in-page] no declared object is named ' + JSON.stringify(args.name) + '.'); }
|
|
674
|
+
var res = r.placeDeclaredAt(d, args.point);
|
|
675
|
+
r.repaint();
|
|
676
|
+
return res;
|
|
677
|
+
}
|
|
678
|
+
throw new Error('[FrqtlHostedEyes/in-page] unknown act door ' + JSON.stringify(args.door) + '.');
|
|
679
|
+
};
|
|
680
|
+
|
|
681
|
+
/** A page refusal `{ refused: { message, record } }` → a thrown Error carrying `.record`, as in a page. @private */
|
|
682
|
+
function _rethrowRefused(out) {
|
|
683
|
+
if (out && out.refused) {
|
|
684
|
+
var e = new Error(out.refused.message);
|
|
685
|
+
e.record = out.refused.record;
|
|
686
|
+
throw e;
|
|
687
|
+
}
|
|
688
|
+
return out;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
/** A page folder (text | base64 per path) → { path: string | Uint8Array }. @private */
|
|
692
|
+
function _folderFromPage(out) {
|
|
693
|
+
if (out.empty) { return { empty: true }; }
|
|
694
|
+
var files = {};
|
|
695
|
+
Object.keys(out.files).forEach(function (k) {
|
|
696
|
+
var v = out.files[k];
|
|
697
|
+
files[k] = (v.text !== undefined) ? v.text : new Uint8Array(Buffer.from(v.base64, 'base64'));
|
|
698
|
+
});
|
|
699
|
+
return { files: files, record: out.record, clips: out.clips };
|
|
700
|
+
}
|
|
701
|
+
|
|
609
702
|
// ── open() ────────────────────────────────────────────────────────────────
|
|
610
703
|
|
|
611
704
|
/**
|
|
@@ -617,8 +710,9 @@ import { libPathOf } from '../../../shim/project/specifiers.js';
|
|
|
617
710
|
* world the source's co-init call built). Supplying any of them is an EXPECTATION, checked
|
|
618
711
|
* after boot and THROWN on mismatch — never a request; a stated bound that is not a
|
|
619
712
|
* positive integer is refused before a browser is acquired.
|
|
620
|
-
* @param {{kind:'project', files:Object<string,string>, entry
|
|
621
|
-
* REGENERATE-grade scene: a project, linked in the page by its one door.
|
|
713
|
+
* @param {{kind:'project', files:Object<string,string>, entry?:string}} opts.sceneSource — the
|
|
714
|
+
* REGENERATE-grade scene: a project, linked in the page by its one door. `entry` is optional when
|
|
715
|
+
* `files` carries the frqtl.json that names it (a different one is refused).
|
|
622
716
|
* @param {object} [opts.renderState] — the declared LOOK, ferried: a renderState bag (for a
|
|
623
717
|
* rung, `effectiveRenderState(rung, mode)`; or a saved project's bag). Its `renderer` boots
|
|
624
718
|
* the page; its rows, camera and backdrop are applied by `FrqtlPage.applyRenderState` before
|
|
@@ -654,7 +748,7 @@ import { libPathOf } from '../../../shim/project/specifiers.js';
|
|
|
654
748
|
/** @private — the body; see `open` for the contract. */
|
|
655
749
|
function _open(opts) {
|
|
656
750
|
var C = _contracts();
|
|
657
|
-
_assertHostable(opts);
|
|
751
|
+
var entry = _assertHostable(opts);
|
|
658
752
|
_assertBoundShape(opts.sceneRecord, C.RS);
|
|
659
753
|
var declared = _declaredRenderState(opts, opts.sceneRecord, C.RST);
|
|
660
754
|
if (!opts.pageUrl || typeof opts.pageUrl !== 'string') {
|
|
@@ -725,7 +819,7 @@ import { libPathOf } from '../../../shim/project/specifiers.js';
|
|
|
725
819
|
})
|
|
726
820
|
.then(function () {
|
|
727
821
|
return page.evaluate(_IN_PAGE_APPLY,
|
|
728
|
-
{ files: opts.sceneSource.files, entry:
|
|
822
|
+
{ files: opts.sceneSource.files, entry: entry, sourceRef: sourceRef, renderState: declared });
|
|
729
823
|
})
|
|
730
824
|
.then(function (applied) {
|
|
731
825
|
return page.evaluate(_IN_PAGE_DISPLAY).then(function (disp) {
|
|
@@ -860,7 +954,7 @@ import { libPathOf } from '../../../shim/project/specifiers.js';
|
|
|
860
954
|
);
|
|
861
955
|
inst.attachCaptureHost(host);
|
|
862
956
|
|
|
863
|
-
return projectIdentity({ files: projectBytes(opts.sceneSource.files) }).then(function (sha) {
|
|
957
|
+
return projectIdentity({ files: projectBytes(linkedFiles(opts.sceneSource.files)) }).then(function (sha) {
|
|
864
958
|
inst.setSourceSha256(sha);
|
|
865
959
|
return inst.sceneRecord();
|
|
866
960
|
}).then(function (nodeScene) {
|
|
@@ -875,7 +969,7 @@ import { libPathOf } from '../../../shim/project/specifiers.js';
|
|
|
875
969
|
'them. (FAIL-LOUD — a hosted still filed under the wrong scene verifies ' +
|
|
876
970
|
'forever against the wrong header.)');
|
|
877
971
|
}
|
|
878
|
-
return _handle(browser, page, host, inst, nodeScene, gate, timeout);
|
|
972
|
+
return _handle(browser, page, host, inst, nodeScene, gate, timeout, modules);
|
|
879
973
|
});
|
|
880
974
|
}).catch(function (e) {
|
|
881
975
|
// A half-opened handle that leaked a browser would poison the zero-orphans gate
|
|
@@ -894,7 +988,7 @@ import { libPathOf } from '../../../shim/project/specifiers.js';
|
|
|
894
988
|
}
|
|
895
989
|
|
|
896
990
|
/** The consumer-facing handle. @private */
|
|
897
|
-
function _handle(browser, page, host, inst, scene, gate, timeout) {
|
|
991
|
+
function _handle(browser, page, host, inst, scene, gate, timeout, modules) {
|
|
898
992
|
var self = {
|
|
899
993
|
/**
|
|
900
994
|
* The SAME face as everywhere else: getScene / getStill / stages / toPNG, and
|
|
@@ -907,7 +1001,7 @@ import { libPathOf } from '../../../shim/project/specifiers.js';
|
|
|
907
1001
|
/** The node-side SceneRecord (page-extended) + its content id. */
|
|
908
1002
|
scene: scene,
|
|
909
1003
|
|
|
910
|
-
/** @returns {Promise<number
|
|
1004
|
+
/** @returns {Promise<number>} the world's own tick (runner.worldTick: 0 before its first step) — READ, never advanced. */
|
|
911
1005
|
tick: function () { return page.evaluate(_IN_PAGE_TICK); },
|
|
912
1006
|
|
|
913
1007
|
/**
|
|
@@ -926,7 +1020,7 @@ import { libPathOf } from '../../../shim/project/specifiers.js';
|
|
|
926
1020
|
'non-negative integer coordinate; got ' + String(n) + '. (FAIL-LOUD.)'));
|
|
927
1021
|
}
|
|
928
1022
|
return gate.opened.then(function () { return self.tick(); }).then(function (now) {
|
|
929
|
-
var at =
|
|
1023
|
+
var at = now;
|
|
930
1024
|
if (at > n) {
|
|
931
1025
|
throw new Error('[FrqtlHostedEyes.driveToTick] the hosted world is ' +
|
|
932
1026
|
'already at tick ' + at + ', past ' + n + '. A world runs forward ' +
|
|
@@ -1010,6 +1104,42 @@ import { libPathOf } from '../../../shim/project/specifiers.js';
|
|
|
1010
1104
|
return file;
|
|
1011
1105
|
},
|
|
1012
1106
|
|
|
1107
|
+
/**
|
|
1108
|
+
* The Record door, forwarded to the PAGE's facade (lib/frqtl/objects/record/Record.js): the folder is
|
|
1109
|
+
* the one the page's own door writes, so a node host and a page produce the same files for the same acts.
|
|
1110
|
+
* The handle has no re-authoring door: a Record opened by `eyes.record.start()` closes only by
|
|
1111
|
+
* `eyes.record.stop()`. A refusal carries `err.record = { changes, clips }`, as in a page.
|
|
1112
|
+
* start() exp.startRecord() → the Record's tick (0)
|
|
1113
|
+
* film(opts) rec.film(opts) → its clip spans. Drive the world while it films:
|
|
1114
|
+
* `const f = eyes.record.film(o); await eyes.driveToTick(n); await f`
|
|
1115
|
+
* stop() rec.stop() → { files: { path: string | Uint8Array }, record, clips } | { empty: true }
|
|
1116
|
+
*/
|
|
1117
|
+
record: {
|
|
1118
|
+
start: function () {
|
|
1119
|
+
return page.evaluate(_IN_PAGE_RECORD_START).then(_rethrowRefused).then(function (o) { return o.tick; });
|
|
1120
|
+
},
|
|
1121
|
+
film: function (opts) {
|
|
1122
|
+
var begun = page.evaluate(_IN_PAGE_RECORD_FILM_BEGIN, opts);
|
|
1123
|
+
gate.opened = begun.then(null, function () {});
|
|
1124
|
+
return begun.then(function () { return page.evaluate(_IN_PAGE_RECORD_FILM_END); });
|
|
1125
|
+
},
|
|
1126
|
+
stop: function () {
|
|
1127
|
+
return page.evaluate(_IN_PAGE_RECORD_STOP, { modules: modules }).then(_rethrowRefused).then(_folderFromPage);
|
|
1128
|
+
}
|
|
1129
|
+
},
|
|
1130
|
+
|
|
1131
|
+
/**
|
|
1132
|
+
* Acts through the page's own doors (the writer's rows come from these, never from node):
|
|
1133
|
+
* toSketch({ by }) exp.toSketch
|
|
1134
|
+
* setDeclared(name, patch, { by }) exp.setDeclared
|
|
1135
|
+
* placeDeclaredAt(name, [x, y, z?]) runner.placeDeclaredAt on the declared object of that name
|
|
1136
|
+
*/
|
|
1137
|
+
act: {
|
|
1138
|
+
toSketch: function (opts) { return page.evaluate(_IN_PAGE_ACT, { door: 'toSketch', opts: opts }); },
|
|
1139
|
+
setDeclared: function (name, patch, opts) { return page.evaluate(_IN_PAGE_ACT, { door: 'setDeclared', name: name, patch: patch, opts: opts }); },
|
|
1140
|
+
placeDeclaredAt: function (name, point) { return page.evaluate(_IN_PAGE_ACT, { door: 'placeDeclaredAt', name: name, point: point }); }
|
|
1141
|
+
},
|
|
1142
|
+
|
|
1013
1143
|
/** Close the managed browser. Symmetric with open(), idempotent. */
|
|
1014
1144
|
dispose: function () { return host.dispose(); }
|
|
1015
1145
|
};
|
|
@@ -1020,5 +1150,46 @@ import { libPathOf } from '../../../shim/project/specifiers.js';
|
|
|
1020
1150
|
|
|
1021
1151
|
var FrqtlHostedEyes = { open: open };
|
|
1022
1152
|
|
|
1023
|
-
|
|
1153
|
+
/**
|
|
1154
|
+
* writeRecordFolder(folder, dir) — write a Record folder (what `eyes.record.stop()` resolves) to `dir`, each path
|
|
1155
|
+
* verbatim: text as UTF-8, clip bytes as they are. The ONE node writer of the folder.
|
|
1156
|
+
* REFUSES (throws): an empty Record (`{ empty: true }` is a state with no folder); a `dir` that exists and is not
|
|
1157
|
+
* empty (a Record is never written over another); a path that is absolute or climbs out of `dir`.
|
|
1158
|
+
* @param {{ files: Object<string, string|Uint8Array> }} folder
|
|
1159
|
+
* @param {string} dir
|
|
1160
|
+
* @returns {string[]} the paths written, in the folder's order
|
|
1161
|
+
*/
|
|
1162
|
+
function writeRecordFolder(folder, dir) {
|
|
1163
|
+
if (folder && folder.empty === true) {
|
|
1164
|
+
throw new Error('[FrqtlHostedEyes] writeRecordFolder: this Record is empty — no change and no clip — a state with no folder. (FAIL-LOUD.)');
|
|
1165
|
+
}
|
|
1166
|
+
if (!folder || !folder.files || typeof folder.files !== 'object') {
|
|
1167
|
+
throw new Error('[FrqtlHostedEyes] writeRecordFolder(folder, dir): folder is what eyes.record.stop() resolves, { files, record, clips }. (FAIL-LOUD.)');
|
|
1168
|
+
}
|
|
1169
|
+
if (typeof dir !== 'string' || !dir) { throw new Error('[FrqtlHostedEyes] writeRecordFolder(folder, dir): dir is a directory path. (FAIL-LOUD.)'); }
|
|
1170
|
+
const root = path.resolve(dir);
|
|
1171
|
+
if (fs.existsSync(root) && fs.readdirSync(root).length) {
|
|
1172
|
+
throw new Error('[FrqtlHostedEyes] writeRecordFolder: ' + root + ' exists and is not empty — a Record is never written over another; ' +
|
|
1173
|
+
'name a new directory. (FAIL-LOUD.)');
|
|
1174
|
+
}
|
|
1175
|
+
const paths = Object.keys(folder.files);
|
|
1176
|
+
for (const p of paths) {
|
|
1177
|
+
const at = path.resolve(root, p);
|
|
1178
|
+
if (path.isAbsolute(p) || (at !== root && !at.startsWith(root + path.sep))) {
|
|
1179
|
+
throw new Error('[FrqtlHostedEyes] writeRecordFolder: the folder path ' + JSON.stringify(p) + ' is not inside the folder. (FAIL-LOUD.)');
|
|
1180
|
+
}
|
|
1181
|
+
const v = folder.files[p];
|
|
1182
|
+
if (typeof v !== 'string' && !(v instanceof Uint8Array)) {
|
|
1183
|
+
throw new Error('[FrqtlHostedEyes] writeRecordFolder: ' + JSON.stringify(p) + ' is neither text nor bytes. (FAIL-LOUD.)');
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
for (const p of paths) {
|
|
1187
|
+
const at = path.resolve(root, p);
|
|
1188
|
+
fs.mkdirSync(path.dirname(at), { recursive: true });
|
|
1189
|
+
fs.writeFileSync(at, folder.files[p]);
|
|
1190
|
+
}
|
|
1191
|
+
return paths;
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
export { FrqtlHostedEyes, writeRecordFolder };
|
|
1024
1195
|
|