syndicate-js/packages/core/src/runtime/dataflow.ts

165 lines
5.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/// SPDX-License-Identifier: GPL-3.0-or-later
/// SPDX-FileCopyrightText: Copyright © 2016-2023 Tony Garnock-Jones <tonyg@leastfixedpoint.com>
// 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<ObjectId> {
recordObservation(objectId: ObjectId): void;
recordDamage(objectId: ObjectId): void;
}
export class Graph<SubjectId, ObjectId> implements ObservingGraph<ObjectId> {
readonly edgesForward: FlexMap<ObjectId, FlexSet<SubjectId>>;
readonly edgesReverse: FlexMap<SubjectId, FlexSet<ObjectId>>;
readonly subjectIdCanonicalizer: Canonicalizer<SubjectId>;
readonly objectIdCanonicalizer: Canonicalizer<ObjectId>;
damagedNodes: FlexSet<ObjectId>;
currentSubjectId: SubjectId | undefined;
constructor(subjectIdCanonicalizer: Canonicalizer<SubjectId>,
objectIdCanonicalizer: Canonicalizer<ObjectId>)
{
this.edgesForward = new FlexMap(objectIdCanonicalizer);
this.edgesReverse = new FlexMap(subjectIdCanonicalizer);
this.subjectIdCanonicalizer = subjectIdCanonicalizer;
this.objectIdCanonicalizer = objectIdCanonicalizer;
this.damagedNodes = new FlexSet(objectIdCanonicalizer);
}
withSubject<T>(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<ObjectId>;
this.edgesReverse.delete(subjectId);
subjectObjects.forEach((oid: ObjectId) => MapSet.del(this.edgesForward, oid, subjectId));
}
observersOf(objectId: ObjectId): Array<SubjectId> {
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<Cell>;
__value: unknown;
static readonly canonicalizer: Canonicalizer<Cell> = v => v.id;
constructor(graph: ObservingGraph<Cell>, initial: unknown) {
this.graph = graph;
this.__value = initial;
}
observe(): unknown {
this.graph.recordObservation(this);
return this.__value;
}
changed() {
this.graph.recordDamage(this);
}
update(newValue: unknown) {
if (!this.valuesEqual(this.__value, newValue)) {
this.changed();
this.__value = newValue;
}
}
abstract valuesEqual(v1: unknown, v2: unknown): boolean;
toString(): string {
return `𝐶<${this.__value}>`;
}
}
export abstract class TypedCell<V> extends Cell {
constructor(graph: ObservingGraph<Cell>, 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<V> extends TypedCell<V> {
readonly name: string | undefined;
constructor(graph: ObservingGraph<Cell>, 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}>`;
}
}