Oracle TextEditorWindowRun uses Java's text editor window and its file dialog as a person does. The open file's text is its Y.Text in the document (plan 7.3): typed is in the file at once, for everyone and for the running engine. Core openText/saveTextAs without Java's mistakes (lost saves after save-as onto a file or a removed file, files named 'ordner/d.txt' or ''); doc files.ts (changeFiles, fileText, nodeFiles, readFilesInto); the room takes a person's files into the engine at once and writes the engine's files back at the tick they changed; the editor's window, Java's dialog, the text area following others' changes with the caret in place, its own undo. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
70 lines
3.7 KiB
TypeScript
70 lines
3.7 KiB
TypeScript
// 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<Record<InstallableProgram, boolean>>;
|
|
readonly settings?: Partial<Record<NodeSettingsKey, unknown>>;
|
|
}
|
|
|
|
/** 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<Id, NodeRunState>, origin: unknown = SIM_ORIGIN): void {
|
|
const nodes = doc.getMap<Y.Map<unknown>>(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<Y.Map<unknown>>;
|
|
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<unknown>;
|
|
// 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);
|
|
}
|