aweft

recipes/schema/main.ts

One file of recipes/schema, at commit 9a5bb24.

// A task board that two nodes share, and neither of them will hold wrongly.
//
// The job is an ordinary one: a board with columns and tasks, edited here and mirrored on a
// second node. What makes it a proof is where the description is enforced. The board carries
// a guard, so a bad edit written here never lands and never reaches the mirror. The mirror
// carries no guard and asks `check` at its own door instead, because a node that did not make
// a commit still has to decide whether to take it. The same description answers both.
//
// Run: node recipes/schema/main.ts

import {
	RefusedError, apply, atomic, createArray, createMap, createObject, fromSnapshot, observer,
	snapshot, textIdOf,
} from '@aweftjs/core';
import type { Change, Commit, ObservableMap } from '@aweftjs/core';
import { check, guard, list, shape, table } from '@aweftjs/schema';
import type { Refusal, StandardSchema } from '@aweftjs/schema';

// --- the validators, written here against the interface ----------------------------------

const rule = (test: (value: unknown) => string | undefined): StandardSchema => ({
	'~standard': {
		version: 1,
		vendor: 'task-board',
		validate: (value) => {
			const problem = test(value);
			return problem === undefined ? { value } : { issues: [{ message: problem }] };
		},
	},
});

const words = (min: number, max: number): StandardSchema =>
	rule((value) => {
		if (typeof value !== 'string') return 'expected some text';
		if (value.trim().length < min) return `expected at least ${min} characters of text`;
		if (value.length > max) return `expected at most ${max} characters`;
		return undefined;
	});

const count = (min: number, max: number): StandardSchema =>
	rule((value) => {
		if (typeof value !== 'number' || !Number.isInteger(value)) return 'expected a whole number';
		return value < min || value > max ? `expected a number between ${min} and ${max}` : undefined;
	});

const flag = rule((value) => (typeof value === 'boolean' ? undefined : 'expected true or false'));

const maybe = (inner: StandardSchema): StandardSchema => ({
	'~standard': {
		version: 1,
		vendor: 'task-board',
		validate: (value) => (value === undefined ? { value } : inner['~standard'].validate(value)),
	},
});

// --- the description ---------------------------------------------------------------------

const Board = shape({
	title: words(1, 60),
	columns: list(shape({ name: words(1, 24), limit: count(1, 20) })),
	tasks: table(shape({ title: words(1, 80), done: flag, notes: maybe(words(1, 400)) })),
});

// --- the application -----------------------------------------------------------------------

interface Column extends Record<string, unknown> {
	name: string;
	limit: number;
}

interface Task extends Record<string, unknown> {
	title: string;
	done: boolean;
	notes?: string;
}

interface BoardDoc extends Record<string, unknown> {
	title: string;
	columns: Column[];
	tasks: ObservableMap<Task>;
}

const newBoard = (title: string): BoardDoc => createObject<BoardDoc>({
	title,
	columns: createArray<Column>(),
	tasks: createMap<Task>(),
});

const addColumn = (board: BoardDoc, name: string, limit: number): void => {
	board.columns.push(createObject<Column>({ name, limit }));
};

const addTask = (board: BoardDoc, title: string): string => {
	const task = createObject<Task>({ title, done: false });
	board.tasks.add(task);
	return textIdOf(task);
};

// --- the checks ----------------------------------------------------------------------------

let checks = 0;

const ok = (passed: boolean, what: string): void => {
	checks += 1;
	if (!passed) {
		console.error(`FAIL: ${what}`);
		process.exit(1);
	}
};

const refused = (run: () => void): readonly Refusal[] => {
	try {
		run();
	} catch (error) {
		if (error instanceof RefusedError) return error.refusals;
		console.error(`FAIL: expected a refusal, got ${String(error)}`);
		process.exit(1);
	}

	console.error('FAIL: expected a refusal, the change went through');
	return process.exit(1);
};

/** Deep equality, because a snapshot's slots are a plain object and their order is not the
 * document: two nodes that applied the same commits in a different order still hold one board. */
const same = (a: unknown, b: unknown): boolean => {
	if (a === b) return true;
	if (a === null || b === null || typeof a !== 'object' || typeof b !== 'object') return false;

	const left = a as Record<string, unknown>;
	const right = b as Record<string, unknown>;
	const keys = Object.keys(left);
	if (keys.length !== Object.keys(right).length) return false;
	return keys.every((key) => key in right && same(left[key], right[key]));
};

// --- the run ---------------------------------------------------------------------------------

const board = newBoard('release 1');
const stopGuard = guard(board, Board);

// The second node. It starts as a copy of the board, so the two agree about every id, and it
// takes commits at its own door, where nothing has been applied yet: a commit it turns away
// costs it nothing at all.
const mirror = fromSnapshot(snapshot(board)) as BoardDoc;
let arrived = 0;
let turnedAway = 0;

const receive = (commit: Commit): boolean => {
	const problems = check(Board, mirror, commit);
	if (problems.length > 0) {
		turnedAway += 1;
		return false;
	}

	apply(mirror, commit);
	arrived += 1;
	return true;
};

const outbound: Commit[] = [];
observer(board).watch((change: Change) => outbound.push({ deltas: [...change.deltas] }));
const flush = (): void => {
	while (outbound.length > 0) receive(outbound.shift()!);
};

// The good changes. A column and three tasks, each one a whole subtree built and attached in
// one commit, so the description is met at the place it lands rather than where it was made.
addColumn(board, 'doing', 5);
const first = addTask(board, 'write the proof');
addTask(board, 'run the gate');
atomic(() => {
	addColumn(board, 'done', 20);
	addTask(board, 'read it back');
});
board.tasks.get(first)!.done = true;
flush();

ok(board.columns.length === 2, 'the columns did not land');
ok(board.tasks.size === 3, 'the tasks did not land');
ok(board.tasks.get(first)!.done, 'the task was not marked done');
ok(same(snapshot(mirror), snapshot(board)), 'the mirror does not hold the same board');

const settled = snapshot(board);

// A bad local write. The guard refuses it, the document is untouched, and because nothing was
// delivered the mirror never hears about it either.
const emptyTitle = refused(() => { board.title = ''; });
ok(emptyTitle.length === 1, 'an empty title should be one refusal');
ok(emptyTitle[0]!.code === 'invalid', `expected an invalid refusal, got ${String(emptyTitle[0]!.code)}`);
ok(same(emptyTitle[0]!.path, ['title']), 'the refusal did not name the title');
ok(board.title === 'release 1', 'the refused title landed anyway');

// A bad subtree, attached in one commit. Every delta in it is about a slot the description
// names; what is wrong is the value in one of them, and the limit that is missing entirely.
const badColumn = refused(() => { board.columns.push(createObject({ name: '' } as Column)); });
ok(badColumn.length === 2, `a nameless column with no limit should be two refusals, got ${badColumn.length}`);
ok(board.columns.length === 2, 'the refused column landed anyway');

// A block is one commit, so a good change and a bad one in the same block go back together.
refused(() => atomic(() => {
	board.title = 'release 2';
	addTask(board, '');
}));
ok(board.title === 'release 1', 'the good half of a refused block stayed');
ok(board.tasks.size === 3, 'the refused task landed anyway');

flush();
ok(same(snapshot(board), settled), 'the board moved during the refusals');
ok(same(snapshot(mirror), settled), 'a refused commit reached the mirror');

// A commit arriving from somewhere the guard does not cover. It is built on a scratch copy of
// the board, which nothing guards, and handed to the mirror's door, which is where a node
// decides. `check` answers there with nothing applied.
const scratch = fromSnapshot(snapshot(board)) as BoardDoc;
const scratchCommits: Commit[] = [];
const stopScratch = observer(scratch).watch((change: Change) => {
	scratchCommits.push({ deltas: [...change.deltas] });
});

scratch.columns[0]!.limit = 999;
addTask(scratch, 'a fine task from elsewhere');
stopScratch();

const [tooBig, fine] = scratchCommits;
ok(receive(tooBig!) === false, 'the mirror took a commit that breaks the description');
ok(mirror.columns[0]!.limit === 5, 'the refused commit landed on the mirror anyway');
ok(receive(fine!) === true, 'the mirror turned away a good commit');
ok(mirror.tasks.size === 4, 'the good commit did not land on the mirror');

// The same commit at the other end: the guarded board refuses it inside `apply`, before any
// watcher hears anything, so the two nodes agree about what the document may hold.
const heardBefore = outbound.length;
const overTheLimit = refused(() => apply(board, tooBig!));
ok(overTheLimit[0]!.code === 'invalid', 'the guard refused the arriving commit for the wrong reason');
ok(board.columns[0]!.limit === 5, 'the arriving commit landed on the board anyway');
ok(outbound.length === heardBefore, 'a refused arriving commit was delivered to a watcher');

// The board catches up with the task the mirror already has, so both nodes end up holding one
// document that fits the description from both directions.
apply(board, fine!);
// That commit came from the mirror, so handing it straight back is an echo. Suppressing one is
// a link's job and not this program's; here there is simply nothing left to send.
outbound.length = 0;
ok(board.tasks.size === 4, 'the board did not take the good commit');
ok(same(snapshot(board), snapshot(mirror)), 'the two nodes disagree about the board');

// Standalone, with no guard anywhere: the description alone answers for a whole document.
const audit = fromSnapshot(snapshot(board)) as BoardDoc;
const auditCommits: Commit[] = [];
const stopAudit = observer(audit).watch((change: Change) => {
	auditCommits.push({ deltas: [...change.deltas] });
});
audit.tasks.get(first)!.notes = 'the notes field is allowed to be missing, and allowed to be here';
stopAudit();
ok(check(Board, audit, auditCommits[0]!).length === 0, 'an optional field was refused when it was filled');

stopGuard();
board.title = '';
ok(board.title === '', 'the guard kept refusing after it was stopped');

console.log(
	`schema proof: ${checks} checks, ${board.columns.length} columns and ${board.tasks.size} tasks, ` +
	`${arrived} commits taken and ${turnedAway} turned away at the door, ` +
	'every refusal rolled back before any watcher saw it',
);