The bridge, native side

The bridge, native side

The web layer calls native handlers and receives native events. Both directions carry JSON as text, and nothing is marshalled for you.

Registering a handler

application.on("ide.projects.open", closure(params: string) => string {
    let value: json::JsonValue = json::parse(params);
    if value.kind() != "object" {
        return `{"ok":false}`;
    }
    let object: json::JsonObject = value as json::JsonObject;
    let path: string = field_string(object, "path");
    if path.size() == 0 {
        return `{"ok":false,"error":"path required"}`;
    }
    return `{"ok":true}`;
})

The parameter is the raw JSON text of the call's params, or the literal null when there were none. The return value is spliced verbatim into the reply.

Splicing untrusted text into a reply tears down the webview. If a value came from a subprocess or a file, parse it and re-serialize rather than pasting it:

let parsed: json::JsonValue = json::parse(output);
if parsed.kind() != "object" { return `{}`; }
return parsed.to_string();

Escape every dynamic string with new json::JsonString(x).to_string().

By convention {"ok":false} is the universal failure reply and the web side checks result.ok === true.

Pushing events

application.emit("ide.fs.change", `{"path":${path_json}}`)

Naming convention in practice: ns.method for request and reply, ns:event for a stream of pushes.

The threading rule

Every handler runs on the bridge dispatch thread, and dispatch is serialized. One slow handler blocks every other handler on every connection.

So a handler that does real work returns immediately and streams the result:

application.on("ide.build.start", closure[application](params: string) => string {
    threads::new_thread(closure[application]() => void {
        let child: process::Process? = spawn_build();
        if child.is_value() {
            let code: number = child.unwrap().wait();
            application.emit("ide.build:event", `{"t":"done","code":${code}}`);
        }
    });
    return `{"ok":true}`;
})

App can be captured into a thread closure, and emit is safe to call from one.

The same applies to anything that blocks: a dialog, a network call, a file walk over a large tree. If it can take more than a few milliseconds, move it off the dispatch thread.

Streaming a child process

threads::new_thread(closure[application, child, event_name]() => void {
    child.on_output(closure[application, event_name](line: string) => void {
        let text: string = new json::JsonString(line).to_string();
        application.emit(event_name, `{"t":"output","text":${text}}`);
    });
    let code: number = child.wait();
    application.emit(event_name, `{"t":"status","state":"idle"}`);
})