graph_state.ts 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541
  1. import {Delta, Transaction} from "./delta";
  2. import {
  3. NodeCreation,
  4. NodeDeletion,
  5. EdgeUpdate,
  6. NewEdge, ExistingEdge,
  7. Target, TargetValue, TargetNode,
  8. } from "./delta";
  9. import {DeltaRegistry} from "./delta_registry";
  10. import {PrimitiveValue} from "./types";
  11. // This interface is used to de-couple the graph state (in our case, a data structure interpreted by the d3 library, and by the 'Graph' react component), from GraphState.
  12. export interface GraphStateListener {
  13. createNode(ns: NodeState): void;
  14. createValue(vs: ValueState): void;
  15. deleteNode(id: PrimitiveValue): void;
  16. deleteValue(value: PrimitiveValue): void;
  17. createLinkToNode(sourceId: PrimitiveValue, label: string, targetId: PrimitiveValue): void;
  18. createLinkToValue(sourceId: PrimitiveValue, label: string, targetValue: PrimitiveValue): void;
  19. deleteLink(sourceId: PrimitiveValue, label: string): void;
  20. }
  21. // A 'proxy' GraphStateListener that multicasts graph state operations to a bunch of GraphStateListeners.
  22. export class FanOutListener implements GraphStateListener {
  23. readonly listeners: GraphStateListener[];
  24. constructor(listeners: GraphStateListener[]) {
  25. this.listeners = listeners;
  26. }
  27. createNode(ns: NodeState) {
  28. this.listeners.forEach(m => m.createNode(ns));
  29. }
  30. createValue(vs: ValueState) {
  31. this.listeners.forEach(m => m.createValue(vs));
  32. }
  33. deleteNode(id: PrimitiveValue) {
  34. this.listeners.forEach(m => m.deleteNode(id));
  35. }
  36. deleteValue(value: PrimitiveValue) {
  37. this.listeners.forEach(m => m.deleteValue(value));
  38. }
  39. createLinkToNode(sourceId: PrimitiveValue, label: string, targetId: PrimitiveValue) {
  40. this.listeners.forEach(m => m.createLinkToNode(sourceId, label, targetId));
  41. }
  42. createLinkToValue(sourceId: PrimitiveValue, label: string, targetValue: PrimitiveValue) {
  43. this.listeners.forEach(m => m.createLinkToValue(sourceId, label, targetValue));
  44. }
  45. deleteLink(sourceId: PrimitiveValue, label: string) {
  46. this.listeners.forEach(m => m.deleteLink(sourceId, label));
  47. }
  48. }
  49. export class DummyListener implements GraphStateListener {
  50. createNode(ns: NodeState) {}
  51. createValue(vs: ValueState) {}
  52. deleteNode(id: PrimitiveValue) {}
  53. deleteValue(value: PrimitiveValue) {}
  54. createLinkToNode(sourceId: PrimitiveValue, label: string, targetId: PrimitiveValue) {}
  55. createLinkToValue(sourceId: PrimitiveValue, label: string, targetValue: PrimitiveValue) {}
  56. deleteLink(sourceId: PrimitiveValue, label: string) {}
  57. }
  58. const DUMMY = new DummyListener();
  59. // Helper
  60. function removeFromArray<T>(arr: T[], cb: (elem: T) => boolean) {
  61. arr.splice(arr.findIndex(cb), 1);
  62. }
  63. abstract class Common {
  64. // For every currently incoming edge, the label of the edge, the delta that set the edge, and the source of the edge.
  65. readonly currentlyIncoming: Array<[string, EdgeUpdate, NodeState]> = [];
  66. // For every previously incoming edge, the delta that set the edge to point somewhere else.
  67. readonly previouslyIncoming: Set<EdgeUpdate> = new Set();
  68. addIncoming(label: string, delta: EdgeUpdate, srcState: NodeState, listener: GraphStateListener) {
  69. this.currentlyIncoming.push([label, delta, srcState]);
  70. }
  71. noLongerIncoming(overwritten: EdgeUpdate, overwriter: EdgeUpdate, listener: GraphStateListener) {
  72. removeFromArray(this.currentlyIncoming, ([_,d]) => d === overwritten);
  73. this.previouslyIncoming.add(overwriter);
  74. }
  75. // only called when undoing a Delta
  76. unAddIncoming(delta: EdgeUpdate, listener: GraphStateListener) {
  77. removeFromArray(this.currentlyIncoming, ([_,d]) => d === delta);
  78. }
  79. // only called when undoing a Delta
  80. incomingAgain(label: string, unOverwritten: EdgeUpdate, srcState: NodeState, unOverwriter: EdgeUpdate, listener: GraphStateListener) {
  81. this.currentlyIncoming.push([label, unOverwritten, srcState]);
  82. this.previouslyIncoming.delete(unOverwriter);
  83. }
  84. getIncomingEdges(): [string, INodeState][] {
  85. return this.currentlyIncoming.map(([label, _, srcState]) => [label, srcState]);
  86. }
  87. // pure
  88. abstract isNode(uuid: PrimitiveValue): boolean;
  89. abstract isValue(value: PrimitiveValue): boolean;
  90. abstract asTarget(): NodeCreation | PrimitiveValue;
  91. // will notify 'listener'
  92. abstract createLinkTo(sourceId: PrimitiveValue, label: string, listener: GraphStateListener): void;
  93. // pure
  94. abstract getDeltaForSetEdge(registry: DeltaRegistry, label: string, target: NodeCreation | PrimitiveValue, reads?: ExistingEdge[]): EdgeUpdate;
  95. abstract getDeltasForDelete(registry: DeltaRegistry): (EdgeUpdate|NodeDeletion)[];
  96. getReads(label: string): EdgeUpdate[] {
  97. return [];
  98. }
  99. // idempotent - may create some new deltas but does not execute them
  100. getIncomingEdgeDependenciesForDelete(registry: DeltaRegistry): [EdgeUpdate[], EdgeUpdate[]]{
  101. const newDeltas = this.currentlyIncoming.map(([label, u, ns]) => {
  102. return registry.newEdgeUpdate(u.overwrite(), null, [], ns.getReads(label).slice());
  103. });
  104. return [[...this.previouslyIncoming].concat(newDeltas), newDeltas];
  105. }
  106. }
  107. // These interfaces allow us to pass around NodeState and ValueState objects while not exposing the stuff that is supposed to remain internal wrt. this module.
  108. interface ICommon {
  109. // array of (label, NodeState) pairs
  110. getIncomingEdges(): [string, INodeState][];
  111. getDeltaForSetEdge(registry: DeltaRegistry, label: string, target: NodeCreation | PrimitiveValue, reads?: ExistingEdge[]): EdgeUpdate;
  112. getDeltasForDelete(registry: DeltaRegistry): (EdgeUpdate|NodeDeletion)[];
  113. asTarget(): NodeCreation | PrimitiveValue;
  114. isNode(uuid: PrimitiveValue): boolean;
  115. isValue(value: PrimitiveValue): boolean;
  116. }
  117. export interface IValueState extends ICommon {
  118. readonly type: "value";
  119. readonly value: PrimitiveValue;
  120. }
  121. export interface INodeState extends ICommon {
  122. readonly type: "node";
  123. readonly outgoingDeltas: ReadonlyMap<string, EdgeUpdate>;
  124. readonly outgoing: ReadonlyMap<string, IValueState | INodeState>;
  125. readonly creation: NodeCreation;
  126. readonly isDeleted: boolean;
  127. }
  128. // In order to edit a graph, we must know what operations most recently "touched" every node, and every edge. This is because new edit operations can depend on earlier operations (that they overwrite).
  129. // This class captures, for a single node, a set of most-recent operations. It also has methods for editing the node. These methods are "pure" (they have no side-effects): they only return Deltas that capture the change. The change doesn't happen until those Deltas are (re)played, with GraphState.
  130. class NodeState extends Common implements INodeState {
  131. readonly type = "node";
  132. readonly creation: NodeCreation;
  133. // For every outgoing edge, the Delta that set this edge to its current value
  134. readonly outgoingDeltas: Map<string, EdgeUpdate> = new Map();
  135. // For every outgoing edge, the set of deltas that read the value of this edge.
  136. // A new EdgeUpdate of this edge must depend ("after") on all these reads.
  137. // Careful: the values (which are arrays) are updated in-place as GraphState (un)executes deltas!
  138. readonly outgoingReads: Map<string, EdgeUpdate[]> = new Map();
  139. // All currently outgoing edges. Edges that were set to null will not be part of this mapping.
  140. readonly outgoing: Map<string, IValueState | INodeState> = new Map();
  141. isDeleted: boolean = false; // has the node been deleted?
  142. constructor(creation: NodeCreation) {
  143. super();
  144. this.creation = creation;
  145. }
  146. isNode(uuid: PrimitiveValue): boolean {
  147. return this.creation.id == uuid;
  148. }
  149. isValue(value: PrimitiveValue): boolean {
  150. return false;
  151. }
  152. asTarget() {
  153. return this.creation;
  154. }
  155. createLinkTo(sourceId: PrimitiveValue, label: string, listener: GraphStateListener) {
  156. listener.createLinkToNode(sourceId, label, this.creation.id);
  157. }
  158. getReads(label: string): EdgeUpdate[] {
  159. return this.outgoingReads.get(label) || [];
  160. }
  161. // Has no side effects - instead returns the deltas that capture the creation or update of the given outgoing edge
  162. getDeltaForSetEdge(registry: DeltaRegistry, label: string, target: NodeCreation | PrimitiveValue, reads: readonly ExistingEdge[] = []): EdgeUpdate {
  163. const previousEdgeUpdate = this.outgoingDeltas.get(label);
  164. if (previousEdgeUpdate === undefined) {
  165. return registry.newEdgeUpdate(this.creation.createOutgoingEdge(label), target, reads, []);
  166. }
  167. else {
  168. return registry.newEdgeUpdate(previousEdgeUpdate.overwrite(), target, reads, this.getReads(label).slice());
  169. }
  170. }
  171. // Has no side effects - instead returns the deltas that capture the deletion of this node (and its incoming+outgoing edges)
  172. getDeltasForDelete(registry: DeltaRegistry): (EdgeUpdate|NodeDeletion)[] {
  173. // set all incoming edges to 'null' (if they aren't already):
  174. const [afterTgt, newDeltas] = this.getIncomingEdgeDependenciesForDelete(registry);
  175. console.log({afterTgt});
  176. // set all outgoing edges to 'null':
  177. const afterSrc = [...this.outgoingDeltas.values()].map(u => {
  178. return registry.newEdgeUpdate(u.overwrite(), null, [], this.getReads(u.overwrites.label).slice());
  179. });
  180. // console.log("deleting", this.creation.id, {afterTgt, afterSrc});
  181. const nodeDeletion = registry.newNodeDeletion(this.creation, afterSrc, afterTgt);
  182. return [...newDeltas, ...afterSrc, nodeDeletion];
  183. }
  184. }
  185. class ValueState extends Common implements IValueState {
  186. readonly type = "value";
  187. shown: boolean = false; // does a (visible) node currently exist for this value?
  188. readonly value: PrimitiveValue;
  189. constructor(value: PrimitiveValue) {
  190. super();
  191. this.value = value;
  192. }
  193. isNode(uuid: PrimitiveValue) {
  194. return false;
  195. }
  196. isValue(value: PrimitiveValue) {
  197. return this.value === value;
  198. }
  199. asTarget() {
  200. return this.value;
  201. }
  202. createLinkTo(sourceId: PrimitiveValue, label: string, listener: GraphStateListener) {
  203. listener.createLinkToValue(sourceId, label, this.value);
  204. }
  205. addIncoming(label: string, delta: EdgeUpdate, srcState: NodeState, listener: GraphStateListener) {
  206. super.addIncoming(label, delta, srcState, listener);
  207. this._showOrHide(listener);
  208. }
  209. noLongerIncoming(overwritten: EdgeUpdate, overwriter: EdgeUpdate, listener: GraphStateListener) {
  210. super.noLongerIncoming(overwritten, overwriter, listener);
  211. this._showOrHide(listener);
  212. }
  213. // only called when undoing a Delta
  214. unAddIncoming(delta: EdgeUpdate, listener: GraphStateListener) {
  215. super.unAddIncoming(delta, listener);
  216. this._showOrHide(listener);
  217. }
  218. // only called when undoing a Delta
  219. incomingAgain(label: string, unOverwritten: EdgeUpdate, srcState: NodeState, unOverwriter: EdgeUpdate, listener: GraphStateListener) {
  220. super.incomingAgain(label, unOverwritten, srcState, unOverwriter, listener);
  221. this._showOrHide(listener);
  222. }
  223. // Value nodes are "always already there", but they are only rendered when they are currently the target of an edge. This function determines whether a value node should be rendered, and creates/deletes the corresponding node using the 'listener'.
  224. private _showOrHide(listener: GraphStateListener) {
  225. const willShow = this.currentlyIncoming.length > 0;
  226. if (!this.shown && willShow) {
  227. listener.createValue(this);
  228. }
  229. else if (this.shown && !willShow) {
  230. listener.deleteValue(this.value);
  231. }
  232. this.shown = willShow;
  233. }
  234. getDeltaForSetEdge(registry: DeltaRegistry, label: string, target: NodeCreation | PrimitiveValue, reads: ExistingEdge[] = []): EdgeUpdate {
  235. // A value cannot be the source of an edge, so we return no deltas.
  236. throw new Error("Assertion failed: A value cannot be the source of an edge.")
  237. }
  238. getDeltasForDelete(registry: DeltaRegistry): (EdgeUpdate|NodeDeletion)[] {
  239. const [edgeUnsettings, _] = this.getIncomingEdgeDependenciesForDelete(registry);
  240. return edgeUnsettings;
  241. }
  242. }
  243. // Executes (primitive) deltas, and updates the graph state accordingly.
  244. // External representations (e.g., d3) can be kept in sync through GraphStateListener.
  245. export class GraphState {
  246. readonly nodes: Map<PrimitiveValue, NodeState> = new Map();
  247. readonly values: Map<PrimitiveValue, ValueState> = new Map();
  248. private deltasSinceCheckpoint: Array<Array<Delta>> = [];
  249. // Deltas that are part of current state
  250. readonly deltas: Set<Delta> = new Set();
  251. // Stores a snapshot of the current graph state. Kind of like Git stash.
  252. pushState() {
  253. this.deltasSinceCheckpoint.push([]);
  254. }
  255. // Restores a previously stored graph state snapshot
  256. popState(): Array<Delta> {
  257. const toUnexec = this.deltasSinceCheckpoint.at(-1);
  258. if (toUnexec === undefined) {
  259. throw new Error("GraphState: cannot popState(), must pushState() first");
  260. }
  261. const result = toUnexec.slice(); // clone this array
  262. toUnexec.reduceRight((_,d)=>{this.unexec(d); return null;}, null);
  263. this.deltasSinceCheckpoint.pop();
  264. return result;
  265. }
  266. exec(delta: Delta, listener: GraphStateListener = DUMMY) {
  267. if (delta instanceof NodeCreation) {
  268. this.execNodeCreation(delta, listener);
  269. }
  270. else if (delta instanceof NodeDeletion) {
  271. this.execNodeDeletion(delta, listener);
  272. }
  273. else if (delta instanceof EdgeUpdate) {
  274. this.execEdgeUpdate(delta, listener);
  275. }
  276. else if (delta instanceof Transaction) {
  277. delta.deltas.forEach(d => this.exec(d, listener));
  278. }
  279. else {
  280. throw new Error("Assertion failed: Unexpected delta type");
  281. }
  282. this.deltasSinceCheckpoint.at(-1)?.push(delta);
  283. this.deltas.add(delta);
  284. }
  285. unexec(delta: Delta, listener: GraphStateListener = DUMMY) {
  286. this.deltas.delete(delta);
  287. if (delta instanceof NodeCreation) {
  288. this.unexecNodeCreation(delta, listener);
  289. }
  290. else if (delta instanceof NodeDeletion) {
  291. this.unexecNodeDeletion(delta, listener);
  292. }
  293. else if (delta instanceof EdgeUpdate) {
  294. this.unexecEdgeUpdate(delta, listener);
  295. }
  296. else if (delta instanceof Transaction) {
  297. delta.deltas.reduceRight((_, d) => {this.unexec(d, listener); return null;}, null);
  298. }
  299. else {
  300. throw new Error("Assertion failed: Unexpected delta type");
  301. }
  302. const checkpoint = this.deltasSinceCheckpoint.at(-1);
  303. if (checkpoint !== undefined) {
  304. const lastDelta = checkpoint.pop()!;
  305. if (lastDelta !== delta) {
  306. throw new Error("GraphState: asymmetrical call to unexec");
  307. }
  308. }
  309. }
  310. private _getEdgeTargetState(target: Target): NodeState | ValueState | undefined {
  311. if (target instanceof TargetNode) {
  312. return this.nodes.get(target.value.id); // may return undefined
  313. }
  314. else if (target instanceof TargetValue && target.value !== null) {
  315. return this.getValueState(target.value);
  316. }
  317. }
  318. private getValueState(value: PrimitiveValue): ValueState {
  319. let vs = this.values.get(value);
  320. if (vs === undefined) {
  321. vs = new ValueState(value);
  322. this.values.set(value, vs);
  323. }
  324. return vs;
  325. }
  326. private execNodeCreation(delta: NodeCreation, listener: GraphStateListener) {
  327. // console.log("execNodeCreation", delta)
  328. const nodeState = new NodeState(delta);
  329. this.nodes.set(delta.id, nodeState);
  330. listener.createNode(nodeState);
  331. }
  332. private unexecNodeCreation(delta: NodeCreation, listener: GraphStateListener) {
  333. // console.log("unexecNodeCreation", delta)
  334. this.nodes.delete(delta.id);
  335. listener.deleteNode(delta.id);
  336. }
  337. private execNodeDeletion(delta: NodeDeletion, listener: GraphStateListener) {
  338. // console.log("execNodeDeletion", delta)
  339. const id = delta.node.id;
  340. const nodeState = this.nodes.get(id);
  341. if (nodeState === undefined) {
  342. throw new Error("Assertion failed: deleted node does not exist")
  343. }
  344. nodeState.isDeleted = true;
  345. listener.deleteNode(id);
  346. }
  347. private unexecNodeDeletion(delta: NodeDeletion, listener: GraphStateListener) {
  348. // restore outgoing links
  349. const id = delta.node.id;
  350. const nodeState = this.nodes.get(id);
  351. if (nodeState === undefined) {
  352. throw new Error("Assertion failed: deleted node does not exist")
  353. }
  354. nodeState.isDeleted = false;
  355. listener.createNode(nodeState);
  356. }
  357. private execEdgeUpdate(delta: EdgeUpdate, listener: GraphStateListener) {
  358. const edge = delta.overwrites;
  359. const label = edge.label;
  360. const sourceId = edge.source.id;
  361. const sourceState = this.nodes.get(sourceId);
  362. if (sourceState === undefined) {
  363. throw new Error(`Assertion failed: Must have sourceState -> cannot execute EdgeUpdate. (${delta.description})`);
  364. }
  365. // Remove edge from old target
  366. if (edge instanceof NewEdge) {
  367. // Nothing was overwritten
  368. }
  369. else if (edge instanceof ExistingEdge) {
  370. const overwrittenUpdate = edge.delta;
  371. const oldTarget = overwrittenUpdate.target;
  372. const oldTargetState = this._getEdgeTargetState(oldTarget);
  373. if (oldTargetState === undefined) {
  374. if (oldTarget.value !== null) {
  375. console.log("TODO: Check: Possibly an assertion error here.");
  376. }
  377. }
  378. else {
  379. oldTargetState.noLongerIncoming(overwrittenUpdate, delta, listener);
  380. listener.deleteLink(sourceId, label);
  381. }
  382. }
  383. // Add edge to new target
  384. const newTarget = delta.target;
  385. const newTargetState = this._getEdgeTargetState(newTarget);
  386. if (newTargetState === undefined) {
  387. if (newTarget.value !== null) {
  388. console.log("TODO: Check: Possibly an assertion error here.");
  389. }
  390. sourceState.outgoing.delete(label);
  391. }
  392. else {
  393. newTargetState.addIncoming(label, delta, sourceState, listener);
  394. newTargetState.createLinkTo(sourceId, label, listener);
  395. sourceState.outgoing.set(label, newTargetState);
  396. }
  397. sourceState.outgoingDeltas.set(label, delta);
  398. // Add reads
  399. for (const r of delta.reads) {
  400. const readsNode = this.nodes.get(r.source.id);
  401. const reads = readsNode!.outgoingReads.get(r.label) || (() => {
  402. const newReads = [];
  403. readsNode!.outgoingReads.set(r.label, newReads);
  404. return newReads;
  405. })();
  406. reads.push(delta);
  407. }
  408. }
  409. private unexecEdgeUpdate(delta: EdgeUpdate, listener: GraphStateListener) {
  410. const edge = delta.overwrites;
  411. const label = edge.label;
  412. const sourceId = edge.source.id;
  413. const sourceState = this.nodes.get(sourceId);
  414. if (sourceState === undefined) {
  415. throw new Error("Assertion failed: Must have sourceState -> cannot un-execute EdgeUpdate.");
  416. }
  417. // Remove reads
  418. for (const r of delta.reads) {
  419. const readsNode = this.nodes.get(r.source.id);
  420. const reads = readsNode!.outgoingReads.get(r.label) || (() => {
  421. const newReads = [];
  422. readsNode!.outgoingReads.set(r.label, newReads);
  423. return newReads;
  424. })();
  425. removeFromArray(reads, elem => elem === delta);
  426. }
  427. // Remove edge from new target
  428. const newTarget = delta.target;
  429. const newTargetState = this._getEdgeTargetState(newTarget);
  430. if (newTargetState === undefined) {
  431. if (newTarget.value !== null) {
  432. console.log("TODO: Check: Possibly an assertion error here.");
  433. }
  434. }
  435. else {
  436. newTargetState.unAddIncoming(delta, listener);
  437. listener.deleteLink(sourceId, label);
  438. }
  439. // Add edge to old target
  440. if (edge instanceof NewEdge) {
  441. // Nothing was overwritten
  442. sourceState.outgoingDeltas.delete(label);
  443. sourceState.outgoing.delete(label);
  444. }
  445. else if (edge instanceof ExistingEdge) {
  446. const overwrittenUpdate = edge.delta;
  447. const oldTarget = overwrittenUpdate.target;
  448. const oldTargetState = this._getEdgeTargetState(oldTarget);
  449. if (oldTargetState === undefined) {
  450. if (oldTarget.value !== null) {
  451. console.log("TODO: Check: Possibly an assertion error here.");
  452. }
  453. sourceState.outgoing.delete(label);
  454. }
  455. else {
  456. oldTargetState.incomingAgain(label, overwrittenUpdate, sourceState, delta, listener);
  457. oldTargetState.createLinkTo(sourceId, label, listener);
  458. sourceState.outgoing.set(label, oldTargetState);
  459. }
  460. sourceState.outgoingDeltas.set(label, overwrittenUpdate);
  461. }
  462. }
  463. }