Moss · guide

What each person is served

The same app, read as it stands on the server for two people. Each gets the screen their app opens on, already drawn.

Install it
pnpm add @niscorp/moss @niscorp/charter @niscorp/nova @niscorp/prism @niscorp/strata @niscorp/tide @niscorp/vex zod @electric-sql/pglite
Step 1 of 4

The app, booted

The same theatre as in the first guide: three screens, a charter, and a shell that opens on the first screen the person has.

The app, booted
import { PGlite } from '@electric-sql/pglite';
import { createServer, defineApp, mintDevToken } from '@niscorp/moss';
import type { ActionDefinition, RenderNode } from '@niscorp/nova';
import { createPglitePool } from '@niscorp/vex/pglite';

const db = new PGlite();
const runtime = { db, pool: createPglitePool(db), session: 'dev-open' as const };
const charter = {
  public: ['programme'],
  member: ['programme', 'seats'],
  manager: { extends: ['member'], allow: ['takings'] },
};
const assignments = { grace: ['member'], ruth: ['manager'] };
const text = (says: string) => ({ component: 'Text', children: says });
const actions: Record<string, ActionDefinition> = {
  programme: { id: 'programme', layout: text('This week: The Tempest') },
  seats: {
    id: 'seats',
    data: { held: 0 },
    triggers: [{ event: 'ui:click', ref: 'hold', do: [{ increment: 'held' }] }],
    layout: {
      component: 'Stack',
      children: [
        text('Seats held: {{$.held}}'),
        { component: 'Button', ref: 'hold', props: { label: 'Hold a seat' } },
      ],
    },
  },
  takings: { id: 'takings', layout: text('Saturday took 4,180') },
};
const app = defineApp({
  charter,
  assignments,
  actions,
  shell: { canvases: [{ id: 'main', initial: ['takings', 'seats', 'programme'] }] },
});
const server = await createServer(app, runtime);
Step 2 of 4

Read a person’s screen

The app runs in a shell on the server, one for each person. shells.snapshot reads that shell as it stands: the tree of drawn nodes that would go down the socket.

A drawn node is a component’s name and its props, or a piece of text. There are no triggers in it and no data: those stay in the shell.

Read a person’s screen
const shells = server.shells;
if (shells === undefined) throw new Error('the app has no shell');

const forGrace = await shells.snapshot(mintDevToken('grace'), 'grace');
console.log(JSON.stringify(forGrace.trees['main']).replace(/act-[0-9a-f-]+/g, 'act-1'));
Step 3 of 4

Just the words

A small function to read the text out of a tree, so two people’s screens can be put side by side.

Just the words
const words = (nodes: readonly RenderNode[]): string[] =>
  nodes.flatMap((node) => (node.type === 'text' ? [node.value] : 'children' in node ? words(node.children) : []));
Step 4 of 4

Two people, two screens

Grace opens on the seats. Ruth, the manager, opens on the takings, which is first on the shell’s list and which only she has. Neither browser is sent anything about the other’s screen.

Two people, two screens
const forRuth = await shells.snapshot(mintDevToken('ruth'), 'ruth');
console.log('grace:', words(forGrace.trees['main'] ?? []));
console.log('ruth:', words(forRuth.trees['main'] ?? []));
server.close();
Then

Run it.

Grace’s screen as the tree Moss would send, with its random id replaced so the line is the same every run, and then the words on each person’s screen.

Run it
npx tsx served.mts
It prints
[{"type":"component","name":"ActionSlot","props":{"instanceId":"act-1","canvasId":"main","definitionId":"seats"},"children":[{"type":"component","name":"Stack","props":{},"children":[{"type":"component","name":"Text","props":{},"children":[{"type":"text","value":"Seats held: 0"}]},{"type":"component","name":"Button","props":{"label":"Hold a seat"},"children":[],"ref":"hold"}]}],"key":"act-1"}]
grace: [ 'Seats held: 0' ]
ruth: [ 'Saturday took 4,180' ]