@m4l-jweb/wrapper 0.6.0 → 0.6.5
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 +119 -16
- package/src/max.d.ts +9 -0
package/package.json
CHANGED
package/src/core.ts
CHANGED
|
@@ -76,15 +76,48 @@ function reload(): void {
|
|
|
76
76
|
*/
|
|
77
77
|
function anything(): void {}
|
|
78
78
|
|
|
79
|
+
/* ------------------------------------------------------------------ *
|
|
80
|
+
* Who to answer
|
|
81
|
+
*
|
|
82
|
+
* A reply normally goes out outlet 0, to the device's OWN [jweb]. But a floating
|
|
83
|
+
* window's [jweb] lives inside a subpatcher, so there is no cord to it - the
|
|
84
|
+
* wrapper reaches it BY NAME, through its [r window-read-<id>] (see loadWindows).
|
|
85
|
+
*
|
|
86
|
+
* So a handler must not hard-code outlet(0): when it is answering a WINDOW it has
|
|
87
|
+
* to route to that window's receiver instead. `window()` below sets replyWindow
|
|
88
|
+
* for the duration of a window message's dispatch, and reply() honours it - so the
|
|
89
|
+
* same get_state()/ui_ready() serve the device view AND any window, unchanged.
|
|
90
|
+
* ------------------------------------------------------------------ */
|
|
91
|
+
|
|
92
|
+
/** The window a reply should go to, or null for the device's own [jweb] (outlet 0). */
|
|
93
|
+
var replyWindow: string | null = null;
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* outlet(0, selector, value) that follows replyWindow - to a window's
|
|
97
|
+
* [r window-read-<id>] when one is set, to the device's own [jweb] otherwise.
|
|
98
|
+
*
|
|
99
|
+
* It takes a FIXED (selector, value) rather than a rest arg, and that is
|
|
100
|
+
* deliberate: `outlet` and `messnamed` are Max HOST functions, and calling
|
|
101
|
+
* `.apply` on them is not reliable across Max builds - when it fails it fails
|
|
102
|
+
* SILENTLY, and an exception here takes the whole ui_ready handshake (mode, build,
|
|
103
|
+
* the state resend) down with it, with no symptom but a device that never fills in
|
|
104
|
+
* its header. Every reply the wrapper sends is one selector and one value, so this
|
|
105
|
+
* is all it ever needs.
|
|
106
|
+
*/
|
|
107
|
+
function reply(selector: string, value: unknown): void {
|
|
108
|
+
if (replyWindow !== null) messnamed("window-read-" + replyWindow, selector, value);
|
|
109
|
+
else outlet(0, selector, value);
|
|
110
|
+
}
|
|
111
|
+
|
|
79
112
|
/**
|
|
80
113
|
* The UI announces it finished loading. The page loads asynchronously, so never
|
|
81
114
|
* assume it was listening when state last changed - resend all of it.
|
|
82
115
|
*/
|
|
83
116
|
function ui_ready(): void {
|
|
84
|
-
|
|
117
|
+
reply("mode", MODE);
|
|
85
118
|
// The UI shows this next to its own baked-in version: a mismatch means a
|
|
86
119
|
// mixed install (stale .amxd instance vs newer extracted UI, or vice versa).
|
|
87
|
-
|
|
120
|
+
reply("build", buildStamp());
|
|
88
121
|
sendCurrentTempo(); // liveapi.ts
|
|
89
122
|
// The device resends its own state here. The page loads asynchronously, so
|
|
90
123
|
// anything sent before it was listening is simply gone.
|
|
@@ -119,7 +152,7 @@ function get_state(id: string): void {
|
|
|
119
152
|
// hands back is what came out of the set. An empty "{}" here after a reopen means
|
|
120
153
|
// the [pattr] did not save - which is a failure with no other symptom.
|
|
121
154
|
post("m4l-jweb: get_state " + id + " -> " + json + "\n");
|
|
122
|
-
|
|
155
|
+
reply("state_" + id, json);
|
|
123
156
|
} catch (e) {
|
|
124
157
|
post("m4l-jweb: get_state error for " + id + " - " + (e as Error).message + "\n");
|
|
125
158
|
}
|
|
@@ -146,11 +179,66 @@ function sync_state(id: string): void {
|
|
|
146
179
|
// bound to nothing), both look exactly like a successful write from here. What the
|
|
147
180
|
// dict says it holds is the only evidence.
|
|
148
181
|
post("m4l-jweb: sync_state " + id + " <- " + json + " (dict now " + d.stringify() + ")\n");
|
|
182
|
+
// BROADCAST the new value to every OTHER view - the device UI and any other
|
|
183
|
+
// window - so a live edit in one page appears in the rest at once. Without this
|
|
184
|
+
// a second page only sees a slot when it next asks for it (on load), which is
|
|
185
|
+
// why an edit in the floating window never reached the device view. The WRITER
|
|
186
|
+
// is skipped: it already applied the value optimistically, and echoing it back
|
|
187
|
+
// could revert the control mid-typing (stateStore has no echo guard).
|
|
188
|
+
broadcastState(id, d.stringify());
|
|
149
189
|
} catch (e) {
|
|
150
190
|
post("m4l-jweb: sync_state error for " + id + " - " + (e as Error).message + "\n");
|
|
151
191
|
}
|
|
152
192
|
}
|
|
153
193
|
|
|
194
|
+
/**
|
|
195
|
+
* Push `state_<id> <json>` to every view except the one that wrote it (replyWindow:
|
|
196
|
+
* a window id, or null for the device view). A window is reached by name, the
|
|
197
|
+
* device view by outlet 0 - the same two paths reply() chooses between.
|
|
198
|
+
*/
|
|
199
|
+
function broadcastState(id: string, json: string): void {
|
|
200
|
+
if (replyWindow !== null) outlet(0, "state_" + id, json); // device view, unless it wrote
|
|
201
|
+
var ids = listWindowIds();
|
|
202
|
+
for (var i = 0; i < ids.length; i++) {
|
|
203
|
+
if (ids[i] !== replyWindow) messnamed("window-read-" + ids[i], "state_" + id, json);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/* ------------------------------------------------------------------ *
|
|
208
|
+
* Floating-window messages
|
|
209
|
+
*
|
|
210
|
+
* A window's page uses the ORDINARY bridge (`outlet(sel, ...)`), so it emits bare
|
|
211
|
+
* selectors just like the device view. They cannot arrive here bare, though: this
|
|
212
|
+
* [js] already has a get_state/ui_ready/etc, and a window's must not be mistaken
|
|
213
|
+
* for the device view's. So the subpatcher TAGS them - `[prepend window <id>]` on
|
|
214
|
+
* the window's [jweb] outlet (see applyWindows in surface.mjs) - and they land
|
|
215
|
+
* here as `window <id> <selector> <args...>`, dispatched on the first word.
|
|
216
|
+
*
|
|
217
|
+
* We then dispatch the INNER selector through the very same handlers, with
|
|
218
|
+
* replyWindow set so any reply routes back to the window and not the device view.
|
|
219
|
+
* That is what gives a window access to the device's state: `get_state`/
|
|
220
|
+
* `sync_state` reach the shared [dict], and `state_<id>` comes back to the window.
|
|
221
|
+
* Anything the library does not know goes to the device's own onWindowMessage().
|
|
222
|
+
* ------------------------------------------------------------------ */
|
|
223
|
+
|
|
224
|
+
function window(id: string): void {
|
|
225
|
+
var selector = String(arguments[1]);
|
|
226
|
+
var args: unknown[] = [];
|
|
227
|
+
for (var i = 2; i < arguments.length; i++) args.push(arguments[i]);
|
|
228
|
+
|
|
229
|
+
var prev = replyWindow;
|
|
230
|
+
replyWindow = id;
|
|
231
|
+
try {
|
|
232
|
+
if (selector === "ui_ready") ui_ready();
|
|
233
|
+
else if (selector === "get_state") get_state(String(args[0]));
|
|
234
|
+
else if (selector === "sync_state") (sync_state as any).apply(null, args);
|
|
235
|
+
else if (typeof onWindowMessage === "function") (onWindowMessage as any).apply(null, [id, selector].concat(args as any[]));
|
|
236
|
+
else post("m4l-jweb: window " + id + " sent unhandled '" + selector + "'\n");
|
|
237
|
+
} finally {
|
|
238
|
+
replyWindow = prev;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
154
242
|
/* ------------------------------------------------------------------ *
|
|
155
243
|
* The self-extracting UI payload
|
|
156
244
|
*
|
|
@@ -179,29 +267,44 @@ function loadWebview(): void {
|
|
|
179
267
|
*
|
|
180
268
|
* A window's [jweb] lives inside a subpatcher, so there is no cord from this
|
|
181
269
|
* [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.
|
|
270
|
+
* it - naming the receiver instead of the cord. The ids come from listWindowIds().
|
|
189
271
|
*/
|
|
190
272
|
function loadWindows(): void {
|
|
191
|
-
if (typeof EXTRA_PAYLOAD_NAMES === "undefined") return;
|
|
192
273
|
var folder = deviceFolder();
|
|
193
274
|
if (!folder) return;
|
|
194
275
|
|
|
195
|
-
var
|
|
276
|
+
var ids = listWindowIds();
|
|
277
|
+
var prefix = windowPrefix();
|
|
278
|
+
for (var i = 0; i < ids.length; i++) {
|
|
279
|
+
var winId = ids[i];
|
|
280
|
+
var winUrl = encodeURI("file:///" + folder + "/" + prefix + winId + ".html") + "?v=" + encodeURIComponent(buildStamp());
|
|
281
|
+
messnamed("window-read-" + winId, "url", winUrl);
|
|
282
|
+
post("m4l-jweb: window " + winId + " -> " + winUrl + "\n");
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/** The `<device>_` prefix a window payload's name carries, so the device's own UI is not one. */
|
|
287
|
+
function windowPrefix(): string {
|
|
288
|
+
return typeof UI_PAYLOAD_NAME !== "undefined" ? UI_PAYLOAD_NAME.replace(/\.html$/, "") + "_" : "";
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* The id of every floating window this device has, from its extracted payloads.
|
|
293
|
+
*
|
|
294
|
+
* Strip the `<device>_` prefix EXPLICITLY: a regex like /^.*_/ is greedy, so a
|
|
295
|
+
* window id with an underscore in it - `edit_grid` - would come back as `grid`.
|
|
296
|
+
*/
|
|
297
|
+
function listWindowIds(): string[] {
|
|
298
|
+
var ids: string[] = [];
|
|
299
|
+
if (typeof EXTRA_PAYLOAD_NAMES === "undefined") return ids;
|
|
300
|
+
var prefix = windowPrefix();
|
|
196
301
|
for (var i = 0; i < EXTRA_PAYLOAD_NAMES.length; i++) {
|
|
197
302
|
var name = EXTRA_PAYLOAD_NAMES[i];
|
|
198
303
|
if (name.slice(-5) !== ".html") continue; // a sample, a preset, ...: not a window
|
|
199
304
|
if (prefix && name.slice(0, prefix.length) !== prefix) continue;
|
|
200
|
-
|
|
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");
|
|
305
|
+
ids.push(name.slice(prefix.length, name.length - 5));
|
|
204
306
|
}
|
|
307
|
+
return ids;
|
|
205
308
|
}
|
|
206
309
|
|
|
207
310
|
function resolveUiUrl(): string | null {
|
package/src/max.d.ts
CHANGED
|
@@ -187,3 +187,12 @@ declare function onMaxurlReply(responseDictName: string): boolean;
|
|
|
187
187
|
|
|
188
188
|
/** Live's tempo changed (and once on attach). */
|
|
189
189
|
declare function onTempoChange(bpm: number): void;
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* A floating window's page sent a selector the library does not handle itself
|
|
193
|
+
* (i.e. not ui_ready/get_state/sync_state). `windowId` is which window; reply with
|
|
194
|
+
* `outlet(0, ...)` and it routes back to that window automatically (see reply() in
|
|
195
|
+
* core.ts). This is how a window editor - a drum map, a browser - talks to the
|
|
196
|
+
* device beyond shared state.
|
|
197
|
+
*/
|
|
198
|
+
declare function onWindowMessage(windowId: string, selector: string, ...args: unknown[]): void;
|