/// SPDX-License-Identifier: GPL-3.0-or-later /// SPDX-FileCopyrightText: Copyright © 2016-2023 Tony Garnock-Jones // Property-based "dataflow" import { FlexSet, FlexMap, Canonicalizer, Value, is } from '@preserves/core'; import { Ref } from './actor.js'; import * as MapSet from './mapset.js'; export interface ObservingGraph { recordObservation(objectId: ObjectId): void; recordDamage(objectId: ObjectId): void; } export class Graph implements ObservingGraph { readonly edgesForward: FlexMap>; readonly edgesReverse: FlexMap>; readonly subjectIdCanonicalizer: Canonicalizer; readonly objectIdCanonicalizer: Canonicalizer; damagedNodes: FlexSet; currentSubjectId: SubjectId | undefined; constructor(subjectIdCanonicalizer: Canonicalizer, objectIdCanonicalizer: Canonicalizer) { this.edgesForward = new FlexMap(objectIdCanonicalizer); this.edgesReverse = new FlexMap(subjectIdCanonicalizer); this.subjectIdCanonicalizer = subjectIdCanonicalizer; this.objectIdCanonicalizer = objectIdCanonicalizer; this.damagedNodes = new FlexSet(objectIdCanonicalizer); } withSubject(subjectId: SubjectId | undefined, f: () => T): T { let oldSubjectId = this.currentSubjectId; this.currentSubjectId = subjectId; let result: T; try { result = f(); } catch (e) { this.currentSubjectId = oldSubjectId; throw e; } this.currentSubjectId = oldSubjectId; return result; } recordObservation(objectId: ObjectId) { if (this.currentSubjectId !== void 0) { MapSet.add(this.edgesForward, objectId, this.currentSubjectId, this.subjectIdCanonicalizer); MapSet.add(this.edgesReverse, this.currentSubjectId, objectId, this.objectIdCanonicalizer); } } recordDamage(objectId: ObjectId) { this.damagedNodes.add(objectId); } forgetSubject(subjectId: SubjectId) { const subjectObjects = this.edgesReverse.get(subjectId) ?? [] as Array; this.edgesReverse.delete(subjectId); subjectObjects.forEach((oid: ObjectId) => MapSet.del(this.edgesForward, oid, subjectId)); } observersOf(objectId: ObjectId): Array { const subjects = this.edgesForward.get(objectId); if (subjects === void 0) return []; return Array.from(subjects); } repairDamage(repairNode: (subjectId: SubjectId) => void) { let repairedThisRound = new FlexSet(this.objectIdCanonicalizer); while (true) { let workSet = this.damagedNodes; this.damagedNodes = new FlexSet(this.objectIdCanonicalizer); const alreadyDamaged = workSet.intersect(repairedThisRound); if (alreadyDamaged.size > 0) { console.warn('Cyclic dependencies involving', alreadyDamaged); } workSet = workSet.subtract(repairedThisRound); repairedThisRound = repairedThisRound.union(workSet); if (workSet.size === 0) break; workSet.forEach(objectId => { this.observersOf(objectId).forEach((subjectId: SubjectId) => { this.forgetSubject(subjectId); this.withSubject(subjectId, () => repairNode(subjectId)); }); }); } } } let __nextCellId = 0; export abstract class Cell { readonly id: string = '' + (__nextCellId++); readonly graph: ObservingGraph; __value: unknown; static readonly canonicalizer: Canonicalizer = v => v.id; constructor(graph: ObservingGraph, initial: unknown) { this.graph = graph; this.__value = initial; } observe(): unknown { this.graph.recordObservation(this); return this.__value; } update(newValue: unknown) { if (!this.valuesEqual(this.__value, newValue)) { this.graph.recordDamage(this); this.__value = newValue; } } abstract valuesEqual(v1: unknown, v2: unknown): boolean; toString(): string { return `𝐶<${this.__value}>`; } } export abstract class TypedCell extends Cell { constructor(graph: ObservingGraph, initial: V) { super(graph, initial); } get value(): V { return this.observe() as V; } set value(v: V) { this.update(v); } abstract valuesEqual(v1: V, v2: V): boolean; } export class Field extends TypedCell { readonly name: string | undefined; constructor(graph: ObservingGraph, initial: V, name?: string) { super(graph, initial); this.name = name; } valuesEqual(v1: V, v2: V): boolean { return is(v1, v2); } toString(): string { return `𝐹<${this.name}=${this.__value}>`; } }