@m4l-jweb/wrapper 0.5.0 → 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/package.json +1 -1
- package/src/core.ts +407 -0
- package/src/max.d.ts +19 -0
package/package.json
CHANGED
package/src/core.ts
CHANGED
|
@@ -91,6 +91,66 @@ function ui_ready(): void {
|
|
|
91
91
|
if (typeof onUiReady === "function") onUiReady();
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
+
/* ------------------------------------------------------------------ *
|
|
95
|
+
* State persistence
|
|
96
|
+
*
|
|
97
|
+
* A declared `state` slot is a named [dict] in the patcher with a [pattr] bound
|
|
98
|
+
* to it (see applyPersistence in surface.mjs). The pattr is what SAVES: Live
|
|
99
|
+
* stores a pattr's value in the set, and restores it before this script runs.
|
|
100
|
+
* The dict is where the app's JSON lives while the set is open.
|
|
101
|
+
*
|
|
102
|
+
* The app never touches the dict directly - it cannot; it is in the patcher. It
|
|
103
|
+
* asks for a slot (`get_state <id>`) and writes one (`sync_state <id> <json>`),
|
|
104
|
+
* and BOTH selectors carry the id as an ARGUMENT, not in the selector name.
|
|
105
|
+
* That matters: Max dispatches a message on its first word, so an app emitting
|
|
106
|
+
* `sync_state_config` looks for a `function sync_state_config()` that no device
|
|
107
|
+
* has, lands in anything(), and is silently swallowed. It did exactly that -
|
|
108
|
+
* every write was dropped, and the read path worked, so the state looked like it
|
|
109
|
+
* simply never persisted. The app side is @m4l-jweb/surface's stateStore().
|
|
110
|
+
* ------------------------------------------------------------------ */
|
|
111
|
+
|
|
112
|
+
/** The app asks for a slot. Reply on the inlet it binds: `state_<id> <json>`. */
|
|
113
|
+
function get_state(id: string): void {
|
|
114
|
+
try {
|
|
115
|
+
var d = new Dict("obj-state-" + id);
|
|
116
|
+
var json = d.stringify();
|
|
117
|
+
// Logged, because this is the ONLY moment that can tell you whether Live actually
|
|
118
|
+
// restored the slot: the app asks for it once the page is up, and what the dict
|
|
119
|
+
// hands back is what came out of the set. An empty "{}" here after a reopen means
|
|
120
|
+
// the [pattr] did not save - which is a failure with no other symptom.
|
|
121
|
+
post("m4l-jweb: get_state " + id + " -> " + json + "\n");
|
|
122
|
+
outlet(0, "state_" + id, json);
|
|
123
|
+
} catch (e) {
|
|
124
|
+
post("m4l-jweb: get_state error for " + id + " - " + (e as Error).message + "\n");
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* The app writes a slot: `sync_state <id> <json...>`.
|
|
130
|
+
*
|
|
131
|
+
* The JSON arrives SPLIT: Max parses a message into atoms on whitespace, so
|
|
132
|
+
* anything the app stringified with spaces in it reaches us as several
|
|
133
|
+
* arguments. Join them back before parsing.
|
|
134
|
+
*/
|
|
135
|
+
function sync_state(id: string): void {
|
|
136
|
+
try {
|
|
137
|
+
var jsonParts: string[] = [];
|
|
138
|
+
for (var i = 1; i < arguments.length; i++) {
|
|
139
|
+
jsonParts.push(String(arguments[i]));
|
|
140
|
+
}
|
|
141
|
+
var json = jsonParts.join(" ");
|
|
142
|
+
var d = new Dict("obj-state-" + id);
|
|
143
|
+
d.parse(json);
|
|
144
|
+
// Read it straight back out. `parse` failing silently, or the dict box not
|
|
145
|
+
// existing at all (it was emitted with an invalid maxclass for a while, so [pattr]
|
|
146
|
+
// bound to nothing), both look exactly like a successful write from here. What the
|
|
147
|
+
// dict says it holds is the only evidence.
|
|
148
|
+
post("m4l-jweb: sync_state " + id + " <- " + json + " (dict now " + d.stringify() + ")\n");
|
|
149
|
+
} catch (e) {
|
|
150
|
+
post("m4l-jweb: sync_state error for " + id + " - " + (e as Error).message + "\n");
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
94
154
|
/* ------------------------------------------------------------------ *
|
|
95
155
|
* The self-extracting UI payload
|
|
96
156
|
*
|
|
@@ -107,11 +167,43 @@ function loadWebview(): void {
|
|
|
107
167
|
if (!url) return;
|
|
108
168
|
outlet(0, "url", url);
|
|
109
169
|
post("m4l-jweb: sent url " + url + "\n");
|
|
170
|
+
|
|
171
|
+
loadWindows();
|
|
110
172
|
} catch (e) {
|
|
111
173
|
post("m4l-jweb: loadWebview error " + (e as Error).message + "\n");
|
|
112
174
|
}
|
|
113
175
|
}
|
|
114
176
|
|
|
177
|
+
/**
|
|
178
|
+
* Point every floating window's [jweb] at its own extracted page.
|
|
179
|
+
*
|
|
180
|
+
* A window's [jweb] lives inside a subpatcher, so there is no cord from this
|
|
181
|
+
* [js] to it. `messnamed` reaches the [r window-read-<id>] the build put next to
|
|
182
|
+
* it - naming the receiver instead of the cord.
|
|
183
|
+
*
|
|
184
|
+
* The window payloads are the extra payloads whose name is `<device>_<id>.html`
|
|
185
|
+
* (packageDevices names them; the device's own UI is UI_PAYLOAD and is not in
|
|
186
|
+
* this list). Strip that prefix EXPLICITLY: a regex like /^.*_/ is greedy, so a
|
|
187
|
+
* window id with an underscore in it - `edit_grid` - would arrive as `grid`, and
|
|
188
|
+
* the page would then load into a receiver nobody is listening on.
|
|
189
|
+
*/
|
|
190
|
+
function loadWindows(): void {
|
|
191
|
+
if (typeof EXTRA_PAYLOAD_NAMES === "undefined") return;
|
|
192
|
+
var folder = deviceFolder();
|
|
193
|
+
if (!folder) return;
|
|
194
|
+
|
|
195
|
+
var prefix = typeof UI_PAYLOAD_NAME !== "undefined" ? UI_PAYLOAD_NAME.replace(/\.html$/, "") + "_" : "";
|
|
196
|
+
for (var i = 0; i < EXTRA_PAYLOAD_NAMES.length; i++) {
|
|
197
|
+
var name = EXTRA_PAYLOAD_NAMES[i];
|
|
198
|
+
if (name.slice(-5) !== ".html") continue; // a sample, a preset, ...: not a window
|
|
199
|
+
if (prefix && name.slice(0, prefix.length) !== prefix) continue;
|
|
200
|
+
var winId = name.slice(prefix.length, name.length - 5);
|
|
201
|
+
var winUrl = encodeURI("file:///" + folder + "/" + name) + "?v=" + encodeURIComponent(buildStamp());
|
|
202
|
+
messnamed("window-read-" + winId, "url", winUrl);
|
|
203
|
+
post("m4l-jweb: window " + winId + " -> " + winUrl + "\n");
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
115
207
|
function resolveUiUrl(): string | null {
|
|
116
208
|
var folder = deviceFolder();
|
|
117
209
|
if (!folder) {
|
|
@@ -265,3 +357,318 @@ function b64decode(s: string): number[] {
|
|
|
265
357
|
}
|
|
266
358
|
return out;
|
|
267
359
|
}
|
|
360
|
+
|
|
361
|
+
/* ------------------------------------------------------------------ *
|
|
362
|
+
* Fetch-to-disk
|
|
363
|
+
*
|
|
364
|
+
* `fetchToFile(url, path)` in the app, [maxurl] in the patcher (the "download"
|
|
365
|
+
* chain), and this, orchestrating between them. Bulk data must never cross the
|
|
366
|
+
* Max message bridge, so the bytes go from libcurl STRAIGHT to disk and only the
|
|
367
|
+
* result comes back as a message.
|
|
368
|
+
*
|
|
369
|
+
* ------------------------------------------------------------------------------
|
|
370
|
+
* THE THREE THINGS THAT MADE THIS LOOK IMPOSSIBLE, all in maxurl's request dict:
|
|
371
|
+
*
|
|
372
|
+
* 1. THE KEY IS `filename_out`. It was `downloadfilename` here, which is not a
|
|
373
|
+
* key maxurl has - and an unknown key in a request dict is IGNORED, not
|
|
374
|
+
* rejected. So the request succeeded (HTTP 200, every time), the body went
|
|
375
|
+
* into the response dict, and nothing was ever written to disk. A perfect
|
|
376
|
+
* success and an empty folder. It is `filename_out` in Max's own reference
|
|
377
|
+
* (docs/refpages/max-ref/maxurl.maxref.xml, the `dictionary` message) and in
|
|
378
|
+
* the maxurl.maxhelp that ships with Live. Do not guess these names.
|
|
379
|
+
*
|
|
380
|
+
* 2. `overwrite_output_file` DEFAULTS TO 0. maxurl refuses to overwrite a file
|
|
381
|
+
* that already exists - so even with the right key, the download works once
|
|
382
|
+
* and then silently stops working. Set it to 1.
|
|
383
|
+
*
|
|
384
|
+
* 3. THE PATH IS AN OS PATH. maxurl is libcurl, not a Max file object: it does
|
|
385
|
+
* not know `~/`, and it does not know Max's `Desktop:/` style. It needs an
|
|
386
|
+
* absolute path, which is why a relative one is resolved against the device
|
|
387
|
+
* folder below.
|
|
388
|
+
*
|
|
389
|
+
* `response_dict` names the reply dict, so what comes back on maxurl's outlets is
|
|
390
|
+
* identifiable rather than a dict called "output" shared with anyone else's request.
|
|
391
|
+
*
|
|
392
|
+
* ------------------------------------------------------------------------------
|
|
393
|
+
* DOWNLOAD, THEN PLACE - why every fetch is TWO maxurl requests.
|
|
394
|
+
*
|
|
395
|
+
* A 404 is a RESPONSE, and maxurl writes it: point `filename_out` at a file you
|
|
396
|
+
* already rely on and a missing URL replaces it with the error page. Measured, not
|
|
397
|
+
* feared - see doc/ARCHITECTURE.md, where a 404 destroyed a good 1.2 MB cached .wav
|
|
398
|
+
* and reported status 404 while doing it. `overwrite_output_file` does not care what
|
|
399
|
+
* the status was.
|
|
400
|
+
*
|
|
401
|
+
* Everywhere else in computing the answer is "write to a temp path and move it into
|
|
402
|
+
* place on success". Max's [js] cannot: its `File` object has open, close, and the
|
|
403
|
+
* read/write family, and NOTHING ELSE. No rename. No delete. (Confirmed twice - in
|
|
404
|
+
* Cycling '74's reference, and by asking the live object what members it has.)
|
|
405
|
+
*
|
|
406
|
+
* So MAXURL DOES THE MOVE. It is libcurl, and libcurl speaks `file://`: a GET of
|
|
407
|
+
* `file:///<temp>` with `filename_out` set to the destination is a native, streaming
|
|
408
|
+
* file copy, on maxurl's own thread, with not one byte passing through [js]. Measured
|
|
409
|
+
* at 6 ms for 1 MB. The download therefore goes:
|
|
410
|
+
*
|
|
411
|
+
* 1. GET <url> -> filename_out <dest>.part (a bad response lands HERE)
|
|
412
|
+
* 2. validate status 2xx, no `error` key, bytes on disk > 0
|
|
413
|
+
* 3. GET file://<part> -> filename_out <dest> (only now is <dest> touched)
|
|
414
|
+
*
|
|
415
|
+
* A failure at 1 or 2 leaves the destination UNTOUCHED, which is the entire point.
|
|
416
|
+
*
|
|
417
|
+
* TWO TRAPS IN STEP 3, both of which look like success:
|
|
418
|
+
*
|
|
419
|
+
* - A file:// reply has NO HTTP STATUS. It comes back `status 0`, which the 2xx
|
|
420
|
+
* check in step 2 would reject as a failure. The copy is validated on BYTES - the
|
|
421
|
+
* destination is the same size as the part file - which is the honest check anyway
|
|
422
|
+
* and the only one that survives both schemes.
|
|
423
|
+
* - The .part file cannot be DELETED afterwards (no unlink, see above), so it is
|
|
424
|
+
* TRUNCATED to zero bytes instead. It costs an inode, not a megabyte, and the next
|
|
425
|
+
* fetch to the same destination overwrites it.
|
|
426
|
+
* ------------------------------------------------------------------ */
|
|
427
|
+
|
|
428
|
+
/** The reply dicts. Named, so nothing else can be mistaken for them - and so the two phases cannot be mistaken for each other. */
|
|
429
|
+
var FETCH_RESPONSE_DICT = "m4ljweb_fetch_response";
|
|
430
|
+
var PLACE_RESPONSE_DICT = "m4ljweb_place_response";
|
|
431
|
+
|
|
432
|
+
/** Where a download lands before it has earned its destination. */
|
|
433
|
+
function partPath(destPath: string): string {
|
|
434
|
+
return destPath + ".part";
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
interface ActiveFetch {
|
|
438
|
+
requestId: string;
|
|
439
|
+
url: string;
|
|
440
|
+
destPath: string;
|
|
441
|
+
/** Bytes in the .part file, carried from the download phase into the place phase. */
|
|
442
|
+
partBytes: number;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// One at a time: [maxurl] can run several, but a queue keeps `currentFetch`
|
|
446
|
+
// unambiguous, and a device downloading its samples wants them in order anyway.
|
|
447
|
+
var fetchQueue: ActiveFetch[] = [];
|
|
448
|
+
var currentFetch: ActiveFetch | null = null;
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* An absolute OS path for libcurl, from whatever the app asked for.
|
|
452
|
+
*
|
|
453
|
+
* A relative path is resolved against the DEVICE's folder - the one place a
|
|
454
|
+
* device can always write, and the same folder the UI payload is extracted into.
|
|
455
|
+
* An absolute path (POSIX `/...`, or Windows `C:/...`) is passed through.
|
|
456
|
+
*/
|
|
457
|
+
function resolveFetchPath(destPath: string): string {
|
|
458
|
+
var isAbsolute = destPath.indexOf("/") === 0 || destPath.indexOf(":") === 1;
|
|
459
|
+
if (isAbsolute) return destPath;
|
|
460
|
+
var folder = deviceFolder();
|
|
461
|
+
return folder ? folder + "/" + destPath : destPath;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/** PHASE 1: download the URL to `<dest>.part`. Nothing touches `<dest>` yet. */
|
|
465
|
+
function processNextFetch(): void {
|
|
466
|
+
if (currentFetch || fetchQueue.length === 0) return;
|
|
467
|
+
var next = fetchQueue.shift();
|
|
468
|
+
if (!next) return;
|
|
469
|
+
currentFetch = next;
|
|
470
|
+
next.destPath = resolveFetchPath(next.destPath);
|
|
471
|
+
|
|
472
|
+
var reqDict = new Dict();
|
|
473
|
+
reqDict.set("url", next.url);
|
|
474
|
+
reqDict.set("http_method", "get");
|
|
475
|
+
reqDict.set("filename_out", partPath(next.destPath)); // NOT the destination - see above
|
|
476
|
+
reqDict.set("overwrite_output_file", 1); // ...or it downloads exactly once, ever
|
|
477
|
+
reqDict.set("response_dict", FETCH_RESPONSE_DICT);
|
|
478
|
+
|
|
479
|
+
post("m4l-jweb: fetch " + next.url + " -> " + next.destPath + "\n");
|
|
480
|
+
// Outlet 1 is the aux outlet; the "download" chain routes `maxurl` off it.
|
|
481
|
+
outlet(1, "maxurl", "dictionary", reqDict.name);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* PHASE 3: ask libcurl to copy the validated .part file over the destination.
|
|
486
|
+
*
|
|
487
|
+
* This is the "move" that [js] cannot do. `file://` is a scheme libcurl handles, so
|
|
488
|
+
* maxurl streams the file on its own thread and nothing crosses the message bridge.
|
|
489
|
+
*/
|
|
490
|
+
function placeFetch(fetched: ActiveFetch): void {
|
|
491
|
+
var reqDict = new Dict();
|
|
492
|
+
reqDict.set("url", encodeURI("file:///" + partPath(fetched.destPath)));
|
|
493
|
+
reqDict.set("http_method", "get");
|
|
494
|
+
reqDict.set("filename_out", fetched.destPath);
|
|
495
|
+
reqDict.set("overwrite_output_file", 1);
|
|
496
|
+
reqDict.set("response_dict", PLACE_RESPONSE_DICT);
|
|
497
|
+
outlet(1, "maxurl", "dictionary", reqDict.name);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
/* ------------------------------------------------------------------ *
|
|
501
|
+
* Samples - the `samples` chain
|
|
502
|
+
* ------------------------------------------------------------------ */
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* The app: `buffer_load <slot> <path>` - read a file into that slot's [buffer~].
|
|
506
|
+
*
|
|
507
|
+
* WHY THIS GOES THROUGH [js] AT ALL, when the chain could route it straight to the
|
|
508
|
+
* buffer: because the path the app wrote is not a path [buffer~] can open, and it
|
|
509
|
+
* fails in the two ways this file exists to prevent.
|
|
510
|
+
*
|
|
511
|
+
* A RELATIVE path is resolved against the device's folder - the same resolution
|
|
512
|
+
* `fetch_to_file` does, and it has to be the same one or the app downloads a file to
|
|
513
|
+
* one place and loads it from another. [buffer~] does not resolve it that way: a bare
|
|
514
|
+
* name is looked up in MAX's SEARCH PATH, which does not contain the device's folder,
|
|
515
|
+
* so `preview.wav` - freshly downloaded, right there next to the .amxd - reports
|
|
516
|
+
* "can't open" and the promise times out.
|
|
517
|
+
*
|
|
518
|
+
* ...and the resolved path CONTAINS SPACES on a normal Live install ("Ableton
|
|
519
|
+
* Library", "Max For Live"). A message travelling through the patcher as text would
|
|
520
|
+
* split there into three atoms and [buffer~] would open the first one. Handed out of
|
|
521
|
+
* [js] as a string, it stays ONE symbol all the way to `replace`.
|
|
522
|
+
*
|
|
523
|
+
* The file is checked before the buffer is asked for it, because a missing file makes
|
|
524
|
+
* [buffer~] print to the Max console and stay silent - there is no failure bang to
|
|
525
|
+
* bind to, and the app would learn nothing until the timeout. `buffer_error` says so
|
|
526
|
+
* at once.
|
|
527
|
+
*/
|
|
528
|
+
function buffer_load(slot: string, path: string): void {
|
|
529
|
+
var resolved = resolveFetchPath(path); // the same folder the download wrote to
|
|
530
|
+
var f: File | null = null;
|
|
531
|
+
try {
|
|
532
|
+
f = new File(resolved, "read");
|
|
533
|
+
} catch (e) {
|
|
534
|
+
f = null;
|
|
535
|
+
}
|
|
536
|
+
if (!f || !f.isopen) {
|
|
537
|
+
outlet(0, "buffer_error", slot, "no file at " + resolved);
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
var bytes = f.eof;
|
|
541
|
+
f.close();
|
|
542
|
+
if (!bytes) {
|
|
543
|
+
outlet(0, "buffer_error", slot, "empty file at " + resolved);
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// Outlet 1 is the aux outlet; the `samples` chain routes `buffer_replace` off it,
|
|
548
|
+
// exactly as the `download` chain routes `maxurl`. Outlet 0 belongs to [jweb].
|
|
549
|
+
post("m4l-jweb: buffer_load " + slot + " -> " + resolved + "\n");
|
|
550
|
+
outlet(1, "buffer_replace", slot, resolved);
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
/** The app: `fetch_to_file <requestId> <url> <destPath>`. */
|
|
554
|
+
function fetch_to_file(requestId: string, url: string, destPath: string): void {
|
|
555
|
+
fetchQueue.push({ requestId: requestId, url: url, destPath: destPath, partBytes: 0 });
|
|
556
|
+
processNextFetch();
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
/**
|
|
560
|
+
* maxurl finished: `maxurl_done dictionary <name>` (outlet 0, via [prepend]).
|
|
561
|
+
*
|
|
562
|
+
* Both phases land here, told apart by the response dict they asked for.
|
|
563
|
+
*/
|
|
564
|
+
function maxurl_done(msgType: string, dictName: string): void {
|
|
565
|
+
if (msgType !== "dictionary") return;
|
|
566
|
+
// A device's own wrapper/device.ts may drive [maxurl] itself (a cache, a spike) and
|
|
567
|
+
// its replies come back down this same cord. Offer it the reply first: without this
|
|
568
|
+
// hook, a request the device made lands here, finds no `currentFetch`, and is
|
|
569
|
+
// dropped in silence - which looks exactly like maxurl never answering.
|
|
570
|
+
if (typeof onMaxurlReply === "function" && onMaxurlReply(dictName)) return;
|
|
571
|
+
if (!currentFetch) return;
|
|
572
|
+
var fetched = currentFetch;
|
|
573
|
+
|
|
574
|
+
try {
|
|
575
|
+
if (dictName === PLACE_RESPONSE_DICT) finishPlace(fetched);
|
|
576
|
+
else finishDownload(fetched, dictName);
|
|
577
|
+
} catch (e) {
|
|
578
|
+
post("m4l-jweb: maxurl_done error - " + (e as Error).message + "\n");
|
|
579
|
+
failFetch(fetched, "wrapper error: " + (e as Error).message);
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
/**
|
|
584
|
+
* PHASE 2: did the download earn its destination?
|
|
585
|
+
*
|
|
586
|
+
* All three checks, and all three are load-bearing: a filesystem failure comes back as
|
|
587
|
+
* status 200 with an `error` key, and a `filename_out` maxurl ignored comes back as a
|
|
588
|
+
* clean 200 with nothing on disk. Only the file itself proves the file.
|
|
589
|
+
*/
|
|
590
|
+
function finishDownload(fetched: ActiveFetch, dictName: string): void {
|
|
591
|
+
var d = new Dict(dictName);
|
|
592
|
+
var status = Number(d.get("status"));
|
|
593
|
+
var errorMsg = d.get("error");
|
|
594
|
+
var bytes = fileSize(partPath(fetched.destPath));
|
|
595
|
+
|
|
596
|
+
if (errorMsg) return failFetch(fetched, String(errorMsg));
|
|
597
|
+
if (status < 200 || status >= 300) return failFetch(fetched, "HTTP " + status);
|
|
598
|
+
if (bytes <= 0) return failFetch(fetched, "HTTP " + status + " but nothing was written to disk");
|
|
599
|
+
|
|
600
|
+
fetched.partBytes = bytes;
|
|
601
|
+
placeFetch(fetched); // ...and only now does the destination get touched
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
/**
|
|
605
|
+
* PHASE 3 (reply): validate the copy ON BYTES, not on status.
|
|
606
|
+
*
|
|
607
|
+
* A `file://` request has no HTTP status - it comes back as `status 0`, measured - so
|
|
608
|
+
* the 2xx check that guards the download would reject a perfectly good copy here. The
|
|
609
|
+
* size is the honest check for both schemes anyway.
|
|
610
|
+
*/
|
|
611
|
+
function finishPlace(fetched: ActiveFetch): void {
|
|
612
|
+
var placed = fileSize(fetched.destPath);
|
|
613
|
+
if (placed !== fetched.partBytes) {
|
|
614
|
+
return failFetch(fetched, "could not place the download: " + placed + " bytes at the destination, expected " + fetched.partBytes);
|
|
615
|
+
}
|
|
616
|
+
truncate(partPath(fetched.destPath)); // [js] cannot DELETE it; zero it instead
|
|
617
|
+
post("m4l-jweb: fetched " + placed + " bytes to " + fetched.destPath + "\n");
|
|
618
|
+
outlet(0, "fetch_done", fetched.requestId, placed);
|
|
619
|
+
currentFetch = null;
|
|
620
|
+
processNextFetch();
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
/** The destination is untouched whenever this is called from phase 1 or 2. That is the point. */
|
|
624
|
+
function failFetch(fetched: ActiveFetch, message: string): void {
|
|
625
|
+
post("m4l-jweb: fetch failed - " + message + "\n");
|
|
626
|
+
outlet(0, "fetch_error", fetched.requestId, message);
|
|
627
|
+
currentFetch = null;
|
|
628
|
+
processNextFetch();
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
/** Bytes on disk, or -1 if there is no readable file there. */
|
|
632
|
+
function fileSize(p: string): number {
|
|
633
|
+
try {
|
|
634
|
+
var f = new File(p);
|
|
635
|
+
if (!f.isopen) return -1;
|
|
636
|
+
var n = f.eof;
|
|
637
|
+
f.close();
|
|
638
|
+
return n;
|
|
639
|
+
} catch (e) {
|
|
640
|
+
return -1;
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
/** Zero a file. The closest thing to `delete` that [js] has - it has no unlink. */
|
|
645
|
+
function truncate(p: string): void {
|
|
646
|
+
try {
|
|
647
|
+
var f = new File(p, "write");
|
|
648
|
+
if (!f.isopen) f.open();
|
|
649
|
+
if (!f.isopen) return;
|
|
650
|
+
f.eof = 0;
|
|
651
|
+
f.close();
|
|
652
|
+
} catch (e) {
|
|
653
|
+
/* a leftover .part is untidy, not a failure */
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
/**
|
|
658
|
+
* maxurl's progress outlet, via [prepend maxurl_progress]:
|
|
659
|
+
*
|
|
660
|
+
* maxurl_progress <response-dict-name> <dl total> <dl now> <ul total> <ul now>
|
|
661
|
+
*
|
|
662
|
+
* FIVE atoms, starting with the dict NAME - per the reference (outlet 1). This
|
|
663
|
+
* used to be read as `(downloaded, total, percent)`, so the "bytes downloaded"
|
|
664
|
+
* the UI showed was actually a symbol, and the total was the download total. And
|
|
665
|
+
* note the reference's warning: not every server sends a content length, so the
|
|
666
|
+
* TOTAL can legitimately be 0. Do not divide by it.
|
|
667
|
+
*/
|
|
668
|
+
function maxurl_progress(dictName: string, dlTotal: number, dlNow: number): void {
|
|
669
|
+
if (!currentFetch) return;
|
|
670
|
+
// The local file:// copy reports progress too. It is not the download, and a UI that
|
|
671
|
+
// showed it would run its progress bar twice - the second time in a few milliseconds.
|
|
672
|
+
if (dictName !== FETCH_RESPONSE_DICT) return;
|
|
673
|
+
outlet(0, "fetch_progress", currentFetch.requestId, dlNow, dlTotal);
|
|
674
|
+
}
|
package/src/max.d.ts
CHANGED
|
@@ -16,6 +16,16 @@ declare function outlet(n: number, ...args: unknown[]): void;
|
|
|
16
16
|
/** Collect the `arguments` of a Max message handler into a real array. */
|
|
17
17
|
declare function arrayfromargs(args: IArguments): unknown[];
|
|
18
18
|
|
|
19
|
+
/**
|
|
20
|
+
* Send a message to every [receive <name>] / [r <name>] in the patcher - the
|
|
21
|
+
* only way [js] can reach a box it has no patch cord to.
|
|
22
|
+
*
|
|
23
|
+
* The wrapper uses it for what CANNOT be wired: a floating window's [jweb] lives
|
|
24
|
+
* inside a subpatcher, so there is no cord from here to there. `messnamed` names
|
|
25
|
+
* the receiver instead of the cord.
|
|
26
|
+
*/
|
|
27
|
+
declare function messnamed(receiveName: string, ...args: unknown[]): void;
|
|
28
|
+
|
|
19
29
|
/** Arguments given to the object box, e.g. `js wrapper.js midi` -> ["midi"]. */
|
|
20
30
|
declare const jsarguments: unknown[];
|
|
21
31
|
|
|
@@ -166,5 +176,14 @@ declare function onUiReady(): void;
|
|
|
166
176
|
/** Every transport poll (20 Hz), after the packaged wrapper has sent its tick. */
|
|
167
177
|
declare function onTick(playing: number, beats: number): void;
|
|
168
178
|
|
|
179
|
+
/**
|
|
180
|
+
* A reply from [maxurl] arrived. Return true if it was YOURS.
|
|
181
|
+
*
|
|
182
|
+
* Only for a device that drives [maxurl] itself - its replies travel the same cord as
|
|
183
|
+
* `fetchToFile()`'s, and the wrapper would otherwise drop them (no `currentFetch`).
|
|
184
|
+
* Match on the `response_dict` name you asked for.
|
|
185
|
+
*/
|
|
186
|
+
declare function onMaxurlReply(responseDictName: string): boolean;
|
|
187
|
+
|
|
169
188
|
/** Live's tempo changed (and once on attach). */
|
|
170
189
|
declare function onTempoChange(bpm: number): void;
|