@m4l-jweb/wrapper 0.9.1 → 0.9.9
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/package.json +1 -1
- package/src/core.ts +90 -37
package/package.json
CHANGED
package/src/core.ts
CHANGED
|
@@ -754,56 +754,107 @@ function placeFetch(fetched: ActiveFetch): void {
|
|
|
754
754
|
}
|
|
755
755
|
|
|
756
756
|
/* ------------------------------------------------------------------ *
|
|
757
|
-
*
|
|
757
|
+
* Save-to-disk
|
|
758
|
+
*
|
|
759
|
+
* The inverse of fetch-to-disk: the APP has the bytes (a rendered WAV), and hands
|
|
760
|
+
* them over base64 in slices (`saveToFile()` in the bridge). We writebytes them into
|
|
761
|
+
* a `<dest>.part` file exactly as extractPayload does - same 16 KB truncation cap, same
|
|
762
|
+
* byte-count verification - then reuse the fetch machinery's PHASE 3 to atomically place
|
|
763
|
+
* `.part` over the destination through [maxurl]. The destination is never touched until
|
|
764
|
+
* the .part has been written AND verified. Requires the `download` chain (for [maxurl]).
|
|
758
765
|
* ------------------------------------------------------------------ */
|
|
759
766
|
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
*/
|
|
784
|
-
function buffer_load(slot: string, path: string): void {
|
|
785
|
-
var resolved = resolveFetchPath(path); // the same folder the download wrote to
|
|
767
|
+
var SAVE_PLACE_RESPONSE_DICT = "m4ljweb_save_place_response";
|
|
768
|
+
|
|
769
|
+
interface ActiveSave {
|
|
770
|
+
requestId: string;
|
|
771
|
+
/** Absolute, resolved destination. */
|
|
772
|
+
destPath: string;
|
|
773
|
+
/** Bytes the app promised in save_begin - the size the .part must match. */
|
|
774
|
+
expect: number;
|
|
775
|
+
/** Bytes actually written so far, for the verify. */
|
|
776
|
+
written: number;
|
|
777
|
+
/** The open .part file, held across the chunk messages. */
|
|
778
|
+
file: File | null;
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
var activeSave: ActiveSave | null = null;
|
|
782
|
+
|
|
783
|
+
/** The app: `save_begin <requestId> <destPath> <byteCount>` - open the .part file. */
|
|
784
|
+
function save_begin(requestId: string, destPath: string, byteCount: number): void {
|
|
785
|
+
// A save already in flight is abandoned - its .part is left for the next place/overwrite.
|
|
786
|
+
if (activeSave && activeSave.file && activeSave.file.isopen) activeSave.file.close();
|
|
787
|
+
|
|
788
|
+
var resolved = resolveFetchPath(destPath);
|
|
789
|
+
var target = partPath(resolved);
|
|
786
790
|
var f: File | null = null;
|
|
787
791
|
try {
|
|
788
|
-
f = new File(
|
|
792
|
+
f = new File(target, "write");
|
|
793
|
+
if (!f.isopen) f.open();
|
|
789
794
|
} catch (e) {
|
|
790
795
|
f = null;
|
|
791
796
|
}
|
|
792
797
|
if (!f || !f.isopen) {
|
|
793
|
-
outlet(0, "
|
|
798
|
+
outlet(0, "save_error", requestId, "cannot open " + target);
|
|
799
|
+
activeSave = null;
|
|
794
800
|
return;
|
|
795
801
|
}
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
802
|
+
f.eof = 0;
|
|
803
|
+
activeSave = { requestId: requestId, destPath: resolved, expect: Number(byteCount), written: 0, file: f };
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
/** The app: `save_chunk <requestId> <base64>` - write one slice. */
|
|
807
|
+
function save_chunk(requestId: string, b64: string): void {
|
|
808
|
+
if (!activeSave || activeSave.requestId !== requestId || !activeSave.file) return;
|
|
809
|
+
var bytes = b64decode(b64);
|
|
810
|
+
var SLICE = 4096; // File.writebytes truncates large calls (~16 KB cap) - same as extractPayload
|
|
811
|
+
for (var off = 0; off < bytes.length; off += SLICE) {
|
|
812
|
+
activeSave.file.writebytes(bytes.slice(off, off + SLICE));
|
|
813
|
+
}
|
|
814
|
+
activeSave.written += bytes.length;
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
/** The app: `save_end <requestId>` - close, verify size, place atomically. */
|
|
818
|
+
function save_end(requestId: string): void {
|
|
819
|
+
if (!activeSave || activeSave.requestId !== requestId) return;
|
|
820
|
+
var save = activeSave;
|
|
821
|
+
if (save.file && save.file.isopen) save.file.close();
|
|
822
|
+
|
|
823
|
+
var onDisk = fileSize(partPath(save.destPath));
|
|
824
|
+
if (onDisk !== save.expect) {
|
|
825
|
+
outlet(0, "save_error", requestId, "size mismatch: wrote " + onDisk + " bytes, expected " + save.expect);
|
|
826
|
+
activeSave = null;
|
|
800
827
|
return;
|
|
801
828
|
}
|
|
829
|
+
// PHASE 3: place the verified .part over the destination via [maxurl]. NOTE the
|
|
830
|
+
// destPath must be a FLAT filename in the device folder - maxurl (libcurl) and Max's
|
|
831
|
+
// [js] File resolve a subdirectory differently, so `sub/x.wav` writes the .part where
|
|
832
|
+
// File agrees but maxurl cannot reach it, and the place returns -1. Keep saves flat.
|
|
833
|
+
var placeUrl = encodeURI("file:///" + partPath(save.destPath));
|
|
834
|
+
post("m4l-jweb: save place " + placeUrl + " -> " + save.destPath + "\n");
|
|
835
|
+
var reqDict = new Dict();
|
|
836
|
+
reqDict.set("url", placeUrl);
|
|
837
|
+
reqDict.set("http_method", "get");
|
|
838
|
+
reqDict.set("filename_out", save.destPath);
|
|
839
|
+
reqDict.set("overwrite_output_file", 1);
|
|
840
|
+
reqDict.set("response_dict", SAVE_PLACE_RESPONSE_DICT);
|
|
841
|
+
outlet(1, "maxurl", "dictionary", reqDict.name);
|
|
842
|
+
}
|
|
802
843
|
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
844
|
+
/** maxurl finished the save's place copy. Validate on bytes, like finishPlace. */
|
|
845
|
+
function finishSavePlace(): void {
|
|
846
|
+
if (!activeSave) return;
|
|
847
|
+
var save = activeSave;
|
|
848
|
+
var placed = fileSize(save.destPath);
|
|
849
|
+
if (placed !== save.expect) {
|
|
850
|
+
outlet(0, "save_error", save.requestId, "could not place save: " + placed + " bytes at destination, expected " + save.expect);
|
|
851
|
+
activeSave = null;
|
|
852
|
+
return;
|
|
853
|
+
}
|
|
854
|
+
truncate(partPath(save.destPath)); // [js] cannot delete; zero it
|
|
855
|
+
post("m4l-jweb: saved " + placed + " bytes to " + save.destPath + "\n");
|
|
856
|
+
outlet(0, "save_done", save.requestId, placed);
|
|
857
|
+
activeSave = null;
|
|
807
858
|
}
|
|
808
859
|
|
|
809
860
|
/** The app: `fetch_to_file <requestId> <url> <destPath>`. */
|
|
@@ -824,6 +875,8 @@ function maxurl_done(msgType: string, dictName: string): void {
|
|
|
824
875
|
// hook, a request the device made lands here, finds no `currentFetch`, and is
|
|
825
876
|
// dropped in silence - which looks exactly like maxurl never answering.
|
|
826
877
|
if (typeof onMaxurlReply === "function" && onMaxurlReply(dictName)) return;
|
|
878
|
+
// A save's place copy comes back on its own dict, and has no `currentFetch` behind it.
|
|
879
|
+
if (dictName === SAVE_PLACE_RESPONSE_DICT) return finishSavePlace();
|
|
827
880
|
if (!currentFetch) return;
|
|
828
881
|
var fetched = currentFetch;
|
|
829
882
|
|