@m4l-jweb/surface 1.2.1 → 1.3.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.
Files changed (2) hide show
  1. package/package.json +2 -2
  2. package/src/index.ts +75 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@m4l-jweb/surface",
3
- "version": "1.2.1",
3
+ "version": "1.3.0",
4
4
  "description": "m4l-jweb: declare a device's Live parameters as code - the surface Push actually sees - plus a mocked-Live dev harness.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -24,7 +24,7 @@
24
24
  ],
25
25
  "peerDependencies": {
26
26
  "react": ">=18",
27
- "@m4l-jweb/bridge": "1.2.1"
27
+ "@m4l-jweb/bridge": "1.3.0"
28
28
  },
29
29
  "peerDependenciesMeta": {
30
30
  "react": {
package/src/index.ts CHANGED
@@ -593,6 +593,81 @@ export function defineWatch<const W extends Record<string, WatchSpec>>(def: Watc
593
593
  return { ...def, keys };
594
594
  }
595
595
 
596
+ /* ------------------------------------------------------------------ *
597
+ * defineFiles - declare that a device touches DISK, once, as code.
598
+ *
599
+ * The third sibling of defineSurface and defineWatch, and the one reality asked
600
+ * for. Writing a file is not one feature, it is three that must travel together:
601
+ *
602
+ * the `download` chain it owns [maxurl], and phase three of saveToFile is a
603
+ * `file://` place through it - even for a device that
604
+ * downloads nothing and only writes bytes it already has;
605
+ * the device folder where the files went, which the page cannot know and
606
+ * the wrapper has to tell it (`device_folder`);
607
+ * the selectors `fetch_to_file`, the `save_*` exchange.
608
+ *
609
+ * Get the first one wrong and the failure is SILENT: the bytes are written
610
+ * correctly, the place request leaves on an aux outlet with nothing on the other
611
+ * end, no reply ever comes, and the promise never settles. The UI sits on
612
+ * "Rendering..." next to a scratch file that looks almost right. Nothing in the
613
+ * build or the tests could see it, because nothing knew the device meant to write.
614
+ *
615
+ * Now it does. A device that declares files gets [maxurl] whether or not its
616
+ * manifest remembered to ask, and gets told its own folder; a device that declares
617
+ * none gets neither.
618
+ *
619
+ * NO SUBFOLDER OPTION, deliberately. A save's destination must be a FLAT name in
620
+ * the device folder: Max's [js] File and maxurl (libcurl) resolve a subdirectory
621
+ * differently, so `sub/x.wav` writes the .part where File agrees and the place
622
+ * cannot reach it (see saveToFile in @m4l-jweb/wrapper). An option for a path that
623
+ * does not work is worse than no option.
624
+ * ------------------------------------------------------------------ */
625
+
626
+ export interface FilesDef {
627
+ /**
628
+ * The page holds the bytes and writes them next to the .amxd - a rendered WAV,
629
+ * an export, a bounce. `saveToFile()` in @m4l-jweb/bridge.
630
+ */
631
+ saves?: boolean;
632
+ /**
633
+ * The page hands Max a URL and Max streams it to disk on its own thread, with
634
+ * nothing crossing the message bridge. `fetchToFile()` in @m4l-jweb/bridge.
635
+ */
636
+ fetches?: boolean;
637
+ /**
638
+ * Tell the page where its files land, at ui_ready (`device_folder <path>`).
639
+ * On by default: a file the user cannot find is a file that was not written, and
640
+ * the page has no other way to learn its own device's folder. Turn it off for a
641
+ * device whose files are its own business - a cache it fills and reads back.
642
+ */
643
+ tellPage?: boolean;
644
+ }
645
+
646
+ export interface Files extends FilesDef {
647
+ readonly saves: boolean;
648
+ readonly fetches: boolean;
649
+ readonly tellPage: boolean;
650
+ }
651
+
652
+ /**
653
+ * Declare what this device does with disk.
654
+ *
655
+ * Like defineSurface and defineWatch, the checks run HERE, at call time, and
656
+ * throw - the build imports this module to derive the chain and the banner, so a
657
+ * bad declaration fails `pnpm build` and CI.
658
+ */
659
+ export function defineFiles(def: FilesDef): Files {
660
+ const saves = def.saves ?? false;
661
+ const fetches = def.fetches ?? false;
662
+ // A declaration that claims neither would still pull in [maxurl] and still tell
663
+ // the page a folder, for a device that touches no disk at all. That is not a
664
+ // harmless default, it is a device carrying a chain nobody can explain.
665
+ if (!saves && !fetches) {
666
+ throw new Error(`files: declare at least one of saves / fetches - a device that does neither should not declare files at all`);
667
+ }
668
+ return { saves, fetches, tellPage: def.tellPage ?? true };
669
+ }
670
+
596
671
  /** How a value is displayed - the parameter's own `format`, or a sane default. */
597
672
  export function formatValue(spec: ParamSpec, value: unknown): string {
598
673
  if (spec.kind === "toggle" || spec.kind === "button") return value ? "on" : "off";