anvilsign in

collin/strudel-claude

1// Everything Strudel expects from a browser that isn't audio.
2//
3// node-web-audio-api's polyfill installs the Web Audio constructors on
4// globalThis and creates a bare `window` object, which covers the audio half.
5// What's left are DOM touchpoints that Strudel reaches for at module scope or in
6// code paths we never take:
7//
8// - @strudel/core's logger dispatches every log line as a `strudel.log`
9// CustomEvent on `document`. That's not incidental — it's the cleanest way
10// for a frontend to consume engine messages, so `document` is a real
11// EventTarget rather than a stub. See subscribeToLogs() below.
12// - superdough's dspworklet.mjs registers a `window.addEventListener('message')`
13// handler at import time, for the user-defined `dsp` feature.
14// - superdough's reverbGen.mjs draws impulse responses to a <canvas> for visual
15// debugging. Never called during playback, but the module must import.
16//
17// `createElement` throws rather than returning a plausible-looking fake: if some
18// path really does need a DOM element, that should surface as an error naming
19// the tag, not as silently wrong behaviour further downstream.
20//
21// The casts below are all of the same kind and all deliberate: TypeScript's DOM
22// lib describes a real browser, and these objects are knowingly partial stand-ins
23// for a handful of members. Widening the globals to `any` would hide genuine
24// mistakes elsewhere, so the lies stay local and named.
25import 'node-web-audio-api/polyfill.js';
26
27/** A log line from either Strudel or superdough. */
28export interface LogEntry {
29 message: string;
30 type?: string;
31 data?: Record<string, unknown>;
32}
33
34type PartialGlobal = Record<string, unknown>;
35
36const noop = (): void => {};
37
38// Node has EventTarget and CustomEvent built in, so `document` can be the real
39// thing — addEventListener/dispatchEvent behave exactly as Strudel expects.
40function makeEventTargetHost(base: PartialGlobal): PartialGlobal {
41 const target = new EventTarget();
42 base.addEventListener = target.addEventListener.bind(target);
43 base.removeEventListener = target.removeEventListener.bind(target);
44 base.dispatchEvent = target.dispatchEvent.bind(target);
45 return base;
46}
47
48const LOCATION = { href: 'http://localhost/', origin: 'http://localhost', protocol: 'http:' };
49
50const win = globalThis.window as unknown as PartialGlobal;
51makeEventTargetHost(win);
52win.postMessage = noop;
53win.location = LOCATION;
54
55if (!globalThis.document) {
56 const doc = makeEventTargetHost({
57 createElement: (tag: string) => {
58 throw new Error(`[browser-shim] document.createElement('${tag}') is not available in Node`);
59 },
60 });
61 globalThis.document = doc as unknown as Document;
62}
63
64// `window.document` is deliberately NOT set, even though `document` is a global.
65//
66// Libraries sniff for a browser by testing `window && window.document &&
67// navigator` — the Anthropic SDK does exactly this, and refuses to construct a
68// client in what it believes is a browser, because that would risk leaking an
69// API key to a web page. Setting `window.document` makes this process look like
70// a browser to every library in it, not just to Strudel, and the chat pane stops
71// working with a confusing error about credentials.
72//
73// The fix is to keep the lie as small as it needs to be, rather than to pass
74// `dangerouslyAllowBrowser` and claim we accept a risk that cannot occur here.
75// Nothing in Strudel or superdough reads `window.document`; they use the bare
76// `document` global, which is still there.
77
78if (!globalThis.location) {
79 globalThis.location = LOCATION as unknown as Location;
80}
81
82/**
83 * Subscribe to engine log messages — both Strudel's (via the `strudel.log` DOM
84 * event) and superdough's (via its setLogger hook, which has no DOM path).
85 *
86 * Returns an unsubscribe function.
87 */
88export async function subscribeToLogs(
89 onLog: (entry: LogEntry) => void,
90): Promise<() => void> {
91 const handler = (e: Event): void => onLog((e as CustomEvent<LogEntry>).detail);
92 globalThis.document.addEventListener('strudel.log', handler);
93
94 const { setLogger } = await import('superdough');
95 setLogger((message, type) => onLog({ message, type }));
96
97 return () => {
98 globalThis.document.removeEventListener('strudel.log', handler);
99 setLogger((msg) => console.log(msg));
100 };
101}