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.
pnpm add @niscorp/moss @niscorp/charter @niscorp/nova @niscorp/prism @niscorp/strata @niscorp/tide @niscorp/vex zod @electric-sql/pgliteThe 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.
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);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.
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'));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.
const words = (nodes: readonly RenderNode[]): string[] =>
nodes.flatMap((node) => (node.type === 'text' ? [node.value] : 'children' in node ? words(node.children) : []));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.
const forRuth = await shells.snapshot(mintDevToken('ruth'), 'ruth');
console.log('grace:', words(forGrace.trees['main'] ?? []));
console.log('ruth:', words(forRuth.trees['main'] ?? []));
server.close();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.
npx tsx served.mts[{"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' ]