# Chapter 18: DOM, Storage, Workers, and JavaScript Interop

> ⚠ **Planned — not in Loup 1.25.** This chapter describes the design in the Language Reference (§2-3.1–§2-3.3 for JavaScript interop, §2-1.10 for workers, §1-16.6.5 for passing data to scripts). The toolchain does not implement it yet, so its listings are design sketches and are not verified.

Sometimes a browser app has to reach past the reactive UI of Chapter 16: call JavaScript, touch the DOM, store data, or move work off the main thread. This chapter covers what the Reference specifies for each and says where it specifies nothing. None of it runs in a browser today; the two checks the compiler already performs are shown with real output, and those listings are checked.

## Calling JavaScript: `extern js`

An `extern js` declaration gives a JavaScript function or value a Loup type (§2-3.1):

```loup,ignore
@mode(web.compiled)

extern js func parseInt(s: string, radix: int): int;
extern js func fetchJson(url: string): Task[string];
extern js val document: Any;
```

*Listing 18-1: JavaScript declarations (design sketch)*

The declared types are trusted, not checked, so a mismatch is a runtime `FfiTypeError`. A function returning a Promise is declared with a `Task[T]` result. A typed array backed by a `SharedArrayBuffer` needs `@unsafe("sharedMemory: …")` on the declaration. `extern js` needs a JavaScript host (`browser`, `node`, `bun`, `deno`), and the toolchain already rejects it in a native program:

```loup,does_not_compile
@mode(host.native)

extern js func parseInt(s: string, radix: int): int;

func main(): void {
    println("never runs");
}
```

```text
main.lphc:3:1: error[E0001]: E_EXTERN_JS_NOT_NATIVE: 'extern js' is not available in 'host.native' or 'freestanding' modes; use 'web.scripted', 'web.compiled', 'host.scripted', or 'host.dual'
```

A native binary has no JavaScript engine, so keep bindings in a web file. There the declaration is accepted today, but calling it fails to type-check (`type of 'parseInt' is not yet known`), because nothing binds it.

## Being called from JavaScript: `@jsexport`

`@jsexport` makes a module-level `func` or `async func` a named export of the compiled module (§2-3.2). Arguments are converted to the declared Loup types (`FfiTypeError` on failure), an `async` export returns a Promise, and the annotation is valid only for the `browser` and `service` targets. A synchronous export must not `await` (`E_JSEXPORT_AWAIT`), and the toolchain checks that already:

```loup,does_not_compile
@mode(web.compiled)

async func fetchSummary(userId: string): Task[string] {
    return "summary for " + userId;
}

@jsexport
func onMessage(userId: string): void {
    let summary = await fetchSummary(userId);
    println(summary);
}

func main(): void {
    onMessage("ann");
}
```

```text
main.lpweb:7:1: error[E0001]: Synchronous @jsexport function must not contain 'await'
```

JavaScript calls a synchronous export synchronously, so it cannot wait. Make the export `async` so JavaScript receives a Promise; `main` becomes `async` too, which ruling MAIN-1 allows:

```loup
@mode(web.compiled)

async func fetchSummary(userId: string): Task[string] {
    return "summary for " + userId;
}

@jsexport
async func onMessage(userId: string): Task[void] {
    let summary = await fetchSummary(userId);
    println(summary);
}

async func main(): Task[void] {
    await onMessage("ann");
}
```

*Listing 18-2: An asynchronous `@jsexport` function*

```text
summary for ann
```

On the interpreter `onMessage` is an ordinary function; nothing is exported yet.

## Typed facades: `lang.interop.js`

For common browser APIs the Reference specifies typed wrappers in `lang.interop.js` and recommends them over raw `extern js` (§2-3.3): `fetch` returning `Task[Response]`; `setTimeout`, `setInterval` and their `clear` functions; and, on the browser target, `querySelector(selector): Option[Element]`, `querySelectorAll`, and `Element` and `Event` classes.

```loup,ignore
@mode(web.compiled)

from lang.interop.js import (querySelector, Event);

func wireDismissButton(): void {
    match querySelector("#dismiss") {
        Some(button) => {
            button.addEventListener("click", func(e: Event): void {
                e.preventDefault();
                button.remove();
            });
        },
        None => printErr("no #dismiss button on this page"),
    }
}
```

*Listing 18-3: DOM access through the typed facade (design sketch)*

A missing element is a `None` you must handle, not a `null` that fails later.

## `web.dom`, `web.storage` and workers

`web.dom` and `web.storage` are reserved names that the Reference mentions (`ShadowRoot` lives in `web.dom`) but does not specify. Only the stdlib requirement documents sketch them, as method lists without signatures (`Element`, `getElementById`, `pushState`; a `Storage` class with `get`, `set`, `remove`, `clear`). No cookie API is specified. This book shows no code for them.

For parallel work the Reference specifies `ScriptWorker[M]` in `concurrent`: an isolated Wasm instance with its own heap that exchanges `Sendable` messages with the page (§2-1.10.2). The worker module exports a `workerMain` function:

```loup,ignore
@mode(web.compiled)

from concurrent import (WorkerChannel);

pub async func workerMain(channel: WorkerChannel[bytes]): Task[void] {
    while (true) {
        let msg = await channel.receive();
        if (msg.is_none()) {
            break;
        }
        channel.reply(checksum(msg.unwrap()));
    }
}
```

*Listing 18-4: A worker module, `src/hasher.lpweb` (design sketch; `checksum` is assumed)*

The page starts it with `await ScriptWorker[bytes].spawn("./hasher.lpweb")`, then uses `send`, `await receive()` and `terminate()`. Messages are copied, never shared, and a worker has no `ui`, `web.dom`, `web.storage` or `extern js` (§2-1.10.5). The `web.workers` API of earlier drafts (`postMessage`, `onMessage`) is not in the Reference. Today there is no `concurrent` module; the prelude's `spawnWorker(path)` runs `workerMain` to completion at the first `receive()` or `join()` and starts no browser worker (Chapter 15).

## Passing data to JavaScript

Never interpolate into an inline `<script>` or `<style>` block: the Reference makes any `{$ … $}` there `E_SCRIPT_STYLE_INTERPOLATION` (§1-16.6.5), because JavaScript and CSS cannot be escaped with a substitution table. Use a `data-` attribute, or the SSR state block (Chapter 19):

````loup
@mode(web.compiled)

func appRoot(userName: string): string {
    let root = ```(html):(none)
    <div id="app" data-user="{$ userName | escape $}"></div>
    ```;
    return root.render({});
}

func main(): void {
    println(appRoot("ann"));
    println(appRoot("\"><img src=x onerror=alert(1)>"));
}
````

*Listing 18-5: Handing a value to the page through a `data-` attribute*

```text
<div id="app" data-user="ann"></div>
<div id="app" data-user="&quot;&gt;&lt;img src=x onerror=alert(1)&gt;"></div>
```

This listing runs today because it only renders a fence to a string. The `<script>` check is not implemented yet, and `html` fences do not escape attribute values yet (Chapter 14), so the listing escapes the value itself with the `escape` filter; the second call shows a hostile value staying inside the attribute.

## Summary

- JavaScript interop, DOM facades, storage and workers are planned (Language Reference §2-3, §2-1.10); Loup 1.25 has no browser runtime.
- `extern js` declares typed JavaScript bindings; the toolchain already reports `E_EXTERN_JS_NOT_NATIVE` in native code.
- `@jsexport` exports Loup functions; the toolchain already rejects `await` in a synchronous export.
- `lang.interop.js` is the specified facade for `fetch`, timers and the DOM; `web.dom` and `web.storage` are reserved but unspecified.
- Workers are `ScriptWorker[M]` with a `workerMain(channel)` entry; today only a batch-mode `spawnWorker` exists.
- Pass data to scripts through `data-` attributes or the SSR state block, never by interpolating into `<script>`.
