aweft

recipes/store/main.ts

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

// A document that outlives the process that wrote it, and a name two openers cannot both take.
//
// The two things `docs/architecture.md` says this proof has to demonstrate: write, kill the
// process, reopen, verify; and find-or-create under concurrent open. The kill is a real
// SIGKILL of a real child, not a close and reopen, because a clean shutdown is exactly the
// case a store gets right by accident.
//
// Run: node recipes/store/main.ts

import { spawnSync } from 'node:child_process';
import { mkdtempSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { fileURLToPath } from 'node:url';

import { atomic, createArray, createObject } from '@aweftjs/core';
import { createStore } from '@aweftjs/store';
import { driverChecks } from '@aweftjs/testing';

import { fileDriver } from './driver-file.ts';

type Doc = Record<string, unknown>;
const here = fileURLToPath(import.meta.url);

// Every store over this directory declares the same paths, because the declaration is what the
// index is built from: one that named fewer would drop the projection rows for the rest
// (design 162).
const DECLARE = { title: ['title'], author: ['meta', 'authorId'] };

const check = (ok: boolean, what: string): void => {
	console.log(`  ${ok ? 'ok  ' : 'FAIL'} ${what}`);
	if (!ok) process.exitCode = 1;
};

// --- the child: writes, then dies without ever being told to stop ----------------------------
if (process.argv[2] === 'write') {
	const store = createStore({
		driver: fileDriver(process.argv[3]!),
		declare: DECLARE,
	});
	const board = await store.open('board');
	const root = board.root as Doc;
	const tasks = createArray<Doc>();
	atomic(() => {
		root.title = 'the board';
		root.tasks = tasks;
		root.meta = createObject<Doc>({ authorId: 'u_7' });
	});
	for (let i = 0; i < 5; i++) tasks.push(createObject<Doc>({ title: `task ${i}`, done: i % 2 === 0 }));
	(tasks[2] as Doc).title = 'task two, renamed';
	await store.settled(board);

	// Nothing is closed, nothing is flushed, and the next line is fatal.
	process.kill(process.pid, 'SIGKILL');
}

// --- the parent -------------------------------------------------------------------------------
const dir = mkdtempSync(join(tmpdir(), 'aweft-store-'));
try {
	console.log('write, kill the process, reopen');
	const child = spawnSync(process.execPath, [here, 'write', dir], { encoding: 'utf8' });
	check(child.signal === 'SIGKILL', `the writer was killed, not shut down (signal ${child.signal})`);

	const store = createStore({
		driver: fileDriver(dir),
		declare: DECLARE,
	});
	const board = await store.open('board');
	const root = board.root as Doc;
	const tasks = root.tasks as Doc[];

	check(root.title === 'the board', 'the document came back');
	check(tasks?.length === 5, `every task came back (${tasks?.length ?? 0} of 5)`);
	check(tasks?.[2]?.title === 'task two, renamed', 'the last write before the kill survived it');
	check(tasks?.[3]?.done === false, 'a value nothing touched is still what it was');

	const history = await store.since('board', 0);
	check(history.length === board.seq, `the history is complete (${history.length} commits)`);
	check(history.every((h, i) => h.seq === i + 1), 'and its sequences have no holes');

	// the reopened document is live: it keeps writing where the dead one left off
	(tasks[0] as Doc).done = true;
	await store.settled(board);
	const after = await createStore({ driver: fileDriver(dir), declare: DECLARE }).open('board');
	check(((after.root as Doc).tasks as Doc[])[0]!.done === true, 'and it carries on being written to');

	console.log('\nqueries survive the kill too');
	check((await store.find({ where: [{ field: 'title', op: 'eq', value: 'the board' }] }))[0]?.doc === 'board',
		'a declared path finds the document the dead process wrote');
	check((await store.find({ where: [{ field: 'author', op: 'eq', value: 'u_7' }] }))[0]?.doc === 'board',
		'and so does one that crosses a nested observable');
	root.title = 'renamed after the kill';
	await store.settled(board);
	check((await store.find({ where: [{ field: 'title', op: 'eq', value: 'the board' }] })).length === 0,
		'the projection moves with the document');
	let refused = false;
	try { await store.find({ where: [{ field: 'nobodyDeclaredThis', op: 'eq', value: 1 }] }); }
	catch { refused = true; }
	check(refused, 'and an undeclared path is refused rather than scanned');

	console.log('\nthe file driver against the conformance suite');
	let red = 0;
	for (const c of driverChecks()) {
		try { await c.run(() => fileDriver(mkdtempSync(join(tmpdir(), 'aweft-conf-')))); }
		catch (e) { red++; console.log(`  FAIL ${c.name}\n       ${(e as Error).message.split('\n')[0]}`); }
	}
	check(red === 0, `a driver written outside the package meets the contract (${driverChecks().length} checks)`);
	check(driverChecks().length === 33, 'thirty-three checks, the number the README states');

	console.log('\nfind-or-create under concurrent open');
	const racers = Array.from({ length: 8 }, () => createStore({ driver: fileDriver(dir), declare: DECLARE }));
	const opened = await Promise.all(racers.map((s) => s.open('contended')));
	const roots = new Set<string>();
	for (const [i, handle] of opened.entries()) {
		(handle.root as Doc)[`w${i}`] = i;
		await racers[i]!.settled(handle);
		roots.add(JSON.stringify((await racers[i]!.since('contended', 0)).length > 0));
	}
	const settled = await createStore({ driver: fileDriver(dir), declare: DECLARE }).open('contended');
	const held = settled.root as Doc;

	check((await createStore({ driver: fileDriver(dir), declare: DECLARE }).open('contended')) !== undefined, 'the name resolves');
	let survived = 0;
	for (let i = 0; i < 8; i++) if (held[`w${i}`] === i) survived++;
	check(survived === 8, `every opener's write survived (${survived} of 8), so they all got one document`);

	await store.stop();
	for (const s of racers) await s.stop();
	void roots;

	console.log(process.exitCode ? '\nFAILED' : '\nall of it holds');
} finally {
	rmSync(dir, { recursive: true, force: true });
}