Call an A11 service from a browser¶
Connect a TypeScript page to a Python echo service over HTTP/2 Server-Sent
Events (SSE). Both peers use the same action schema and named streams, with no
browser-specific request format.
Before you start
Install the Python package and
npm install a11@npm:@curiositystack/a11. Browsers
negotiate HTTP/2 through TLS. Browsers do not support clear-text HTTP/2
prior knowledge (h2c), so local browser testing also requires a trusted
development certificate.
Try it¶
Send a message and inspect what crosses the wire. The inspector records action messages and node fragments; select a row to see its action names, node IDs, and encoded byte size. Half-close says that the client will send no more data while allowing already-sent work to drain. Reconnect creates a fresh transport and session.
1. Define the action contract¶
Both peers must agree on this schema. The Python service declares:
ECHO_SCHEMA = a11.ActionSchema(
name="echo",
description="Return the supplied text unchanged.",
inputs={"input": a11.ActionPortSchema(
name="input", type="text/plain", typeinfo=str, required=True
)},
outputs={"output": a11.ActionPortSchema(
name="output", type="text/plain", typeinfo=str, required=True
)},
)
The browser creates the equivalent ActionSchema and ActionPortSchema,
describing the same runnable action and streaming ports on each side.
const echoSchema = new ActionSchema({
name: 'echo',
inputs: {
input: new ActionPortSchema({
name: 'input', type: 'text/plain', required: true,
}),
},
outputs: {
output: new ActionPortSchema({
name: 'output', type: 'text/plain', required: true,
}),
},
});
2. Implement the server-only handler¶
Only the server registers a handler. Inputs and outputs are AsyncNodes, so
the handler reads the final input value and puts the same value into the
output node:
async def echo(action: a11.Action) -> None:
value = await action["input"].consume(str)
await action["output"].put(value, final=True)
The browser registers the schema without a handler. Calling it therefore creates an action message for the remote session instead of executing code in the page.
3. Prepare and run the Python service¶
Each SSE connection becomes an accepting Session with echo registered as a
handler. The endpoint pair shares the /demos/echo prefix:
registry = a11.ActionRegistry()
registry.register("echo", ECHO_SCHEMA, echo)
async def accept(stream):
session = a11.Session(action_registry=registry)
await session.add_stream(stream, mode="accept")
await session.done.wait()
options = a11.HttpSseOptions()
options.connect_endpoint = "/demos/echo"
server = a11.HttpSseServer.create("127.0.0.1", 80, accept, options)
The complete deployable module is
a11/demos/echo_server.py.
Create and trust a localhost certificate with
mkcert, then run the service with TLS:
mkcert localhost 127.0.0.1 ::1
python -m a11.demos.echo_server \
--host 127.0.0.1 --port 9000 \
--certificate ./localhost+2.pem \
--private-key ./localhost+2-key.pem
4. Create a browser session and connect¶
Create the client registry and session, then attach an SSE stream in START
mode. The server attaches the other end in ACCEPT mode. The stream is created
against an origin, with the two endpoint paths given relative to it:
const registry = new ActionRegistry();
need(registry.register('echo', echoSchema));
const session = need(Session.create({actionRegistry: registry}));
const stream = need(HttpSseClientWireStream.create('https://a11.to', {
connectEndpoint: '/sse/demoserver',
messageEndpoint: '/streams/{id}/message',
}));
need(await session.addStream(stream, StreamMode.START));
5. Wire the interface through AsyncNodes¶
Make the action against the session's shared node map and stream. call()
sends the action description; writing the input and marking it final sends its
node fragments. The response arrives through the output AsyncNode:
const action = need(registry.makeAction('echo', {
nodeMap: session.getNodeMap(), stream, session,
}));
need(await action.call());
const input = need(await action.getInput('input'));
need(await input.finalize(text));
need(await action.waitForDispatch(10_000));
const output = need(await action.getOutput('output', false));
const reply = need(await output.next({timeoutMs: 10_000}));
need(await action.wait(30_000));
The interface field remains an A11 node. Action and session identity persists on both peers, so the page uses the same streaming and completion operations as the service.
6. Display failures¶
A11 APIs return a StatusOr<T>: success values pass isOk, while failures
carry a status code, message, and structured details. Convert failures at the
UI boundary and render them in a live error region: