Nova · guide

An endpoint

How an action asks for something: a named call, what it sends, where the answer goes, and what happens after.

Install it
pnpm add @niscorp/nova @niscorp/prism @niscorp/strata zod
Step 1 of 4

The kit

Nova draws nothing itself. It resolves a layout to a tree, and an adapter’s components draw the tree. This file uses the terminal kit, so it runs anywhere Node does; in an app the registry holds your own components.

The kit
import { createShell, shellSettled, shellView } from '@niscorp/nova';
import type { ActionDefinition } from '@niscorp/nova';
import { createTtyView } from '@niscorp/nova/adapters/tty';
import { defaultRegistry } from '@niscorp/nova/adapters/tty/components';
import { evaluate } from '@niscorp/prism';

const registry = defaultRegistry();
registry.register('CanvasSlot', ({ children }) => ({ lines: children.flatMap((child) => child.lines) }));
Step 2 of 4

An action with an endpoint

An endpoint is declared by name. request is a Prism document that builds the body from the action’s data, response picks what is kept of the answer, and target is where in the data it goes.

A call step invokes it. onSuccess runs after the answer has landed, and onError if it failed, with the failure in scope as @error.

An action with an endpoint
const price: ActionDefinition = {
  id: 'price',
  data: { seats: 2, total: 0, said: 'Not asked yet' },
  layout: {
    component: 'Stack',
    children: [
      { component: 'Text', children: '{{$.seats}} seats: {{$.total}} euros' },
      { component: 'Text', children: '$.said' },
      { component: 'Button', ref: 'ask', children: 'Ask the price' },
    ],
  },
  endpoints: {
    quote: {
      url: 'https://theatre.example/quote',
      method: 'POST',
      request: { seats: { $ref: '$.seats' } },
      response: { $ref: '$.total' },
      target: 'total',
    },
  },
  triggers: [
    {
      event: 'ui:click',
      ref: 'ask',
      do: [
        {
          call: 'quote',
          onSuccess: [{ set: 'said', value: 'Quoted' }],
          onError: [{ set: 'said', value: '{{@error.message}}' }],
        },
      ],
    },
  ],
};
Step 3 of 4

A shell that can reach the world

The document names a URL, and the host decides how it is reached: fetch is the shell’s to give, and transform is what evaluates the request and the response. Here fetch is a stand-in that prices a seat at 45 euros, so the file runs with no server.

A shell that can reach the world
const shell = createShell({
  canvases: [{ id: 'main' }],
  registry,
  actions: { price },
  transform: evaluate,
  fetch: async (_url, init) => new Response(JSON.stringify({ total: JSON.parse(String(init?.body)).seats * 45 })),
});
shell.push('main', 'price');
await shellSettled(shell);
const draw = (): void => console.log(createTtyView(registry, shellView(shell).api).render().text);
draw();
Step 4 of 4

Ask

One press. The request is built, the call is made, the answer is written to total, and then onSuccess runs.

Ask
shell.dispatch({ type: 'ui:click', ref: 'ask' });
await shellSettled(shell);
draw();
Then

Run it.

Before the call and after it. The component that was pressed knows nothing of the endpoint: it said ui:click, and the document did the rest.

Run it
npx tsx endpoint.mts
It prints
── main ────────────────────────────────────────────────────
2 seats: 0 euros
Not asked yet
[1] (Ask the price)
── main ────────────────────────────────────────────────────
2 seats: 90 euros
Quoted
[1] (Ask the price)