@m4l-jweb/bridge 0.1.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 +24 -0
  2. package/src/index.ts +105 -0
package/package.json ADDED
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "@m4l-jweb/bridge",
3
+ "version": "0.1.0",
4
+ "description": "m4l-jweb: the browser-side bridge connecting a device's web UI to Max for Live.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/alienmind/m4l-jweb.git",
10
+ "directory": "packages/bridge"
11
+ },
12
+ "publishConfig": {
13
+ "access": "public"
14
+ },
15
+ "exports": {
16
+ ".": "./src/index.ts"
17
+ },
18
+ "files": [
19
+ "src"
20
+ ],
21
+ "sideEffects": [
22
+ "./src/index.ts"
23
+ ]
24
+ }
package/src/index.ts ADDED
@@ -0,0 +1,105 @@
1
+ /**
2
+ * @m4l-jweb/bridge - the entire API surface between your web app and the device.
3
+ *
4
+ * jweb exposes exactly two calls to the embedded page:
5
+ * window.max.bindInlet(name, handler) - receive a Max message
6
+ * window.max.outlet(...args) - send a Max message
7
+ *
8
+ * That is it. Everything else in this package is ergonomics on top: the
9
+ * `ui_ready` handshake, base64 helpers for structured payloads, and a dev shim
10
+ * so the same code runs in a plain browser with hot reload.
11
+ *
12
+ * Zero dependencies, by design - this is the one piece that ships inside a
13
+ * Chromium view embedded in a DAW.
14
+ */
15
+
16
+ /**
17
+ * Max message arguments are untyped on the wire (numbers, symbols, lists), so
18
+ * handler params arrive as `unknown` and you narrow them at the edge:
19
+ * bindInlet("tempo", (bpm) => setTempo(Number(bpm)))
20
+ */
21
+ type InletHandler = (...args: unknown[]) => void;
22
+
23
+ interface MaxGlobal {
24
+ bindInlet: (name: string, fn: InletHandler) => void;
25
+ outlet: (...args: unknown[]) => void;
26
+ }
27
+
28
+ declare global {
29
+ interface Window {
30
+ max?: MaxGlobal;
31
+ maxSimulate?: (name: string, ...args: unknown[]) => void;
32
+ }
33
+ }
34
+
35
+ const handlers = new Map<string, InletHandler>();
36
+
37
+ /** True inside a real [jweb] view; false in the browser dev shim. */
38
+ export const inJweb: boolean = typeof window !== "undefined" && !!window.max;
39
+
40
+ /**
41
+ * Handle the Max message `name`. Bind every selector your device receives, and
42
+ * keep the names in one `protocol.ts` so both sides of the bridge agree.
43
+ */
44
+ export function bindInlet(name: string, fn: InletHandler): void {
45
+ handlers.set(name, fn);
46
+ if (typeof window !== "undefined" && window.max) {
47
+ window.max.bindInlet(name, fn);
48
+ }
49
+ }
50
+
51
+ /** Send a Max message: a selector word followed by its arguments. */
52
+ export function outlet(...args: unknown[]): void {
53
+ if (typeof window !== "undefined" && window.max) {
54
+ window.max.outlet(...args);
55
+ } else {
56
+ console.debug("[m4l-jweb:outlet]", ...args);
57
+ }
58
+ }
59
+
60
+ /**
61
+ * Announce that the page has finished loading, so the wrapper can send back the
62
+ * current state (mode, build stamp, tempo, parameters).
63
+ *
64
+ * This is NOT optional. The page loads asynchronously: anything the device sent
65
+ * before your handlers were bound is simply gone. Call this once, after
66
+ * binding, and treat the reply as the source of truth.
67
+ */
68
+ export function uiReady(): void {
69
+ outlet("ui_ready");
70
+ }
71
+
72
+ /**
73
+ * Max splits messages on commas and semicolons, so any structured payload -
74
+ * JSON, code, a filesystem path - must be encoded before it crosses the bridge.
75
+ *
76
+ * These are UTF-8 safe: btoa alone throws on anything outside latin1.
77
+ */
78
+ export function encodeBase64(s: string): string {
79
+ const bytes = new TextEncoder().encode(s);
80
+ let binary = "";
81
+ for (const b of bytes) binary += String.fromCharCode(b);
82
+ return btoa(binary);
83
+ }
84
+
85
+ export function decodeBase64(s: string): string {
86
+ const binary = atob(s);
87
+ const bytes = Uint8Array.from(binary, (c) => c.charCodeAt(0));
88
+ return new TextDecoder().decode(bytes);
89
+ }
90
+
91
+ /**
92
+ * Dev shim. Outside Max there is no window.max, so route messages straight into
93
+ * the bound handlers and let the developer drive the device from the console:
94
+ *
95
+ * maxSimulate("tempo", 128)
96
+ * maxSimulate("tick", 1, 4.25)
97
+ */
98
+ if (typeof window !== "undefined" && !window.max) {
99
+ window.maxSimulate = (name, ...args) => {
100
+ const fn = handlers.get(name);
101
+ if (fn) fn(...args);
102
+ else console.warn(`[m4l-jweb] no handler bound for "${name}"`);
103
+ };
104
+ console.info("[m4l-jweb] running outside Max. Drive the device with: maxSimulate('tempo', 128)");
105
+ }