// What a run leaves in the project, written back into the document (plan // §6.1: the room writes it as `origin: 'sim'`), as Java's project holds it // when it is saved after a run (measured with tools/java-oracle's // SaveAfterRunRun): each computer's files as its programs left them, which // servers run, and the settings a run changes — a mail server's domain and // accounts, a mail program's account and folders, a web server's virtual // hosts, a firewall changed through a router's page, the address a DHCP // client got. Only what differs is written, so that a write changes nothing // the second time and a text edited meanwhile keeps what it did not touch. import * as Y from 'yjs'; import type { Id, InstallableProgram, VirtualFileSystem } from '@mdg-filius/core'; import { sameBytes, writeFiles } from './files.js'; import { NODE_SETTINGS, ROOTS } from './projectDoc.js'; export { emailServerAfterRun } from '@mdg-filius/fls'; import { fromY, toY, type PlainValue } from './values.js'; /** The origin of the room's writes (plan §6.1); a person's undo never takes them. */ export const SIM_ORIGIN = 'sim'; /** A node's settings key in the document (NODE_SETTINGS' values). */ export type NodeSettingsKey = (typeof NODE_SETTINGS)[keyof typeof NODE_SETTINGS]; /** What a run left of a node. */ export interface NodeRunState { readonly files?: VirtualFileSystem; readonly active?: Partial>; readonly settings?: Partial>; } /** Plain values alike, whatever the order of an object's fields; a field that is undefined is none. */ function same(left: unknown, right: unknown): boolean { if (left instanceof Uint8Array || right instanceof Uint8Array) return left instanceof Uint8Array && right instanceof Uint8Array && sameBytes(left, right); if (Array.isArray(left) || Array.isArray(right)) { return Array.isArray(left) && Array.isArray(right) && left.length === right.length && left.every((each, index) => same(each, right[index])); } if (typeof left === 'object' && left !== null && typeof right === 'object' && right !== null) { const fields = (value: object) => Object.entries(value).filter(([, field]) => field !== undefined); const leftFields = fields(left); const rightFields = new Map(fields(right)); return leftFields.length === rightFields.size && leftFields.every(([key, field]) => rightFields.has(key) && same(field, rightFields.get(key))); } return left === right; } /** Writes what the runs left of the nodes, what differs only, in one transaction; nodes the document no longer has are left out. */ export function writeRunState(doc: Y.Doc, states: ReadonlyMap, origin: unknown = SIM_ORIGIN): void { const nodes = doc.getMap>(ROOTS.nodes); doc.transact(() => { for (const [id, state] of states) { const node = nodes.get(id); if (node === undefined) continue; if (state.files !== undefined) writeFiles(node, state.files); const programs = node.get('programs') as Y.Map>; for (const [program, active] of Object.entries(state.active ?? {})) { const entry = programs.get(program); if (entry !== undefined && entry.get('active') !== active) entry.set('active', active); } const settings = node.get('settings') as Y.Map; // Only a setting the node has (a router's own web server has none). for (const [key, value] of Object.entries(state.settings ?? {})) { const current = settings.get(key); if (value === undefined || current === undefined) continue; if (!same(fromY(current), value)) settings.set(key, toY(value as PlainValue)); } } }, origin); }