createApp and lifecycle
Create an app, register connect, key, resize, and close handlers, then call listen
createApp() returns a chainable app that you configure with event handlers and then start with .listen(). Nothing runs until you call listen(), which is what wires the app to the host.
import { createApp } from '@voxalsh/sdk';
createApp()
.onConnect(handleConnect)
.onKey(handleKey)
.onResize(handleResize)
.onClose(handleClose)
.listen();Each on* method returns the app, so calls chain, and every handler may be async. One app instance handles every connected user (they are all multiplexed into it), so keep per-user state keyed by conn.id.
A minimal app
import { createApp } from '@voxalsh/sdk';
createApp()
.onConnect((conn) => {
conn.write('\x1b[2J\x1b[H'); // clear screen, home cursor
conn.write('hello from voxal\r\n');
conn.write('press q to quit\r\n');
})
.onKey((conn, data) => {
if (data === 'q' || data === '\x03') return conn.close();
conn.write('you pressed: ' + JSON.stringify(data) + '\r\n');
})
.onClose((conn) => {
console.log('bye', conn.id); // server logs
})
.listen();Handlers
Pass each on* method a function. Passing null or undefined clears that handler, which is handy for conditional registration. Passing any other non-function throws a TypeError at registration, so mistakes surface immediately instead of at the first event.
onConnect(handler)
onConnect(handler: (conn: Conn) => void | Promise<void>): AppFires when a user connects. This is the first event, so use it to build and seed per-connection state and to do the first render. conn.cols and conn.rows already hold the terminal size.
.onConnect((conn) => {
conn.write('welcome, your terminal is ' + conn.cols + 'x' + conn.rows + '\r\n');
})onKey(handler)
onKey(handler: (conn: Conn, data: string) => void | Promise<void>): AppFires on input. data is the decoded input string, usually a single character, but escape sequences and control codes arrive too. See The connection for the full encoding.
.onKey((conn, data) => {
if (data === 'q' || data === '\x03') return conn.close();
if (data === '\x1b[A') return conn.write('up\r\n');
conn.write('you pressed ' + JSON.stringify(data) + '\r\n');
})Tip
If you are matching arrow keys, control codes, and editing keys by hand, consider the UI layer's useInput. It decodes all of that into a friendly key object for you.
onResize(handler)
onResize(handler: (conn: Conn, size: Size) => void | Promise<void>): AppFires on terminal resize. conn.cols and conn.rows are already updated before your handler runs, and the new size is also passed as { cols, rows }.
.onResize((conn, size) => {
conn.write('\x1b[2J\x1b[H'); // clear and home
conn.write('-'.repeat(size.cols) + '\r\n'); // redraw to fit
})onClose(handler)
onClose(handler: (conn: Conn) => void | Promise<void>): AppFires when the user disconnects, whether they dropped the session or your app called conn.close(). Clean up per-connection state here.
const sessions = new Map();
createApp()
.onConnect((conn) => sessions.set(conn.id, { score: 0 }))
.onClose((conn) => sessions.delete(conn.id))
.listen();listen()
listen(): AppActivates the app. Call it exactly once, after registering handlers. Calling listen() on a second app within one bundle logs a warning and the last one wins, since only one app per bundle is supported.
Error handling
The SDK does not await your handlers, so events are not ordered relative to your own async work. Guard your own awaits with try/catch.
Thrown errors and rejected promises from your handlers are caught and logged to the server logs (prefixed [voxal app error]), not the user's terminal, so one bad handler can never crash the shared sandbox. To show the user something, write to the terminal yourself.
Managing per-connection state
All users of your app share one sandbox, so keep per-session data in a Map keyed by conn.id, and free it in onClose.
const state = new Map();
createApp()
.onConnect((conn) => {
state.set(conn.id, { count: 0 });
render(conn);
})
.onKey((conn, data) => {
const s = state.get(conn.id);
if (data === '+') s.count++;
render(conn);
})
.onClose((conn) => state.delete(conn.id))
.listen();
function render(conn) {
const { count } = state.get(conn.id);
conn.write('\x1b[2J\x1b[Hcount: ' + count + '\r\n');
}Module-level values like state above are shared across all connections and reset when the app scales to zero and re-boots. See How voxal works for the details.