primitive_delta.ts 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659
  1. import {inspect} from "util"; // NodeJS library
  2. import {createHash} from "crypto";
  3. import {Buffer} from "buffer";
  4. import * as _ from "lodash";
  5. import {UUID, PrimitiveValue} from "./types";
  6. import {bufferXOR} from "./buffer_xor";
  7. import {Delta} from "./delta";
  8. export interface PrimitiveDelta extends Delta {
  9. }
  10. export class NodeCreation implements PrimitiveDelta {
  11. readonly id: UUID;
  12. readonly hash: Buffer;
  13. readonly description: string;
  14. // Inverse dependency: Deletions of this node.
  15. deletions: Array<NodeDeletion> = []; // append-only
  16. // Inverse dependency: Creation outgoing edges.
  17. outgoingEdges: Array<EdgeCreation> = []; // append-only
  18. // Inverse dependency: All the times this node was the target of an edge update.
  19. incomingEdges: Array<EdgeCreation | EdgeUpdate> = []; // append-only
  20. constructor(hash: Buffer, id: UUID) {
  21. this.hash = hash;
  22. this.id = id;
  23. this.description = "NEW("+this.id.value.toString().slice(0,8)+")";
  24. }
  25. getDependencies(): [] {
  26. return [];
  27. }
  28. getTypedDependencies(): [] {
  29. return [];
  30. }
  31. getConflicts(): [] {
  32. return [];
  33. }
  34. getHash(): Buffer {
  35. return this.hash;
  36. }
  37. getDescription(): string {
  38. return this.description;
  39. }
  40. // pretty print to console under NodeJS
  41. [inspect.custom](depth: number, options: object) {
  42. return "NodeCreation{" + inspect(this.id, options) + "}";
  43. }
  44. toString(): string {
  45. return this[inspect.custom](0, {});
  46. }
  47. serialize(): any {
  48. return {
  49. type: "NodeCreation",
  50. id: this.id.value,
  51. }
  52. }
  53. *iterPrimitiveDeltas(): Iterable<Delta> {
  54. yield this;
  55. }
  56. }
  57. export class NodeDeletion implements PrimitiveDelta {
  58. readonly hash: Buffer;
  59. readonly description: string;
  60. // Dependency: The node being deleted.
  61. readonly creation: NodeCreation;
  62. // Dependency: All outgoing edges of the deleted node must be deleted also.
  63. readonly deletedOutgoingEdges: Array<EdgeCreation | EdgeUpdate>;
  64. // Dependency: For every time the deleted node was target of an edge, the deletion depends on the EdgeUpdate that sets this edge to have a different target.
  65. readonly afterIncomingEdges: Array<EdgeUpdate | NodeDeletion>;
  66. // Conflicts: Concurrent deletion of the same node.
  67. deleteConflicts: Array<NodeDeletion> = [];
  68. // Conflicts: Concurrent creation of an edge with as source the deleted node.
  69. edgeSourceConflicts: Array<EdgeCreation> = [];
  70. // Conflicts: Concurrent update and deletion of an edge (because its source node is deleted).
  71. updateConflicts: Array<EdgeUpdate | NodeDeletion> = [];
  72. // Conflicts: Concurrent creation/update of an edge with as target the deleted node.
  73. edgeTargetConflicts: Array<EdgeCreation | EdgeUpdate> = [];
  74. // Parameters:
  75. // deletedOutgoingEdges: For every outgoing edge of this node being deleted, must explicitly specify the most recent EdgeCreation/EdgeUpdate on this edge, to make it explicit that this deletion happens AFTER the EdgeCreation/EdgeUpdate (instead of concurrently, which is a conflict).
  76. // afterIncomingEdges: For every edge that is or was (once) incoming to this node, must explicitly specify an EdgeUpdate/NodeDeletion that makes this edge point somewhere else (no longer to this node).
  77. constructor(hash: Buffer, creation: NodeCreation, deletedOutgoingEdges: Array<EdgeCreation|EdgeUpdate>, afterIncomingEdges: Array<EdgeUpdate|NodeDeletion>) {
  78. this.hash = hash;
  79. this.creation = creation;
  80. this.deletedOutgoingEdges = deletedOutgoingEdges;
  81. this.afterIncomingEdges = afterIncomingEdges;
  82. // Check some assertions
  83. if (_.uniq(deletedOutgoingEdges).length !== deletedOutgoingEdges.length) {
  84. throw new Error("Assertion failed: deletedOutgoingEdges contains duplicates.");
  85. }
  86. if (_.uniq(afterIncomingEdges).length !== afterIncomingEdges.length) {
  87. throw new Error("Assertion failed: deletedOutgoingEdges contains duplicates.");
  88. }
  89. for (const supposedlyOutgoingEdge of this.deletedOutgoingEdges) {
  90. if (supposedlyOutgoingEdge.getCreation().source !== this.creation) {
  91. throw new Error("Assertion failed: Every element of delOutgoings must be an EdgeCreation or EdgeUpdate of an outgoing edge of the deleted node.")
  92. }
  93. }
  94. for (const supposedlyIncomingEdge of this.afterIncomingEdges) {
  95. if (supposedlyIncomingEdge instanceof NodeDeletion) {
  96. // should check if the NodeDeletion deletes an edge that was *once*
  97. let isReallyIncomingEdge = false;
  98. for (const deletedEdge of supposedlyIncomingEdge.deletedOutgoingEdges) {
  99. let current: EdgeCreation|EdgeUpdate|undefined = deletedEdge;
  100. while (current !== undefined) {
  101. if (current.target.getTarget() === this.creation) {
  102. isReallyIncomingEdge = true;
  103. break;
  104. }
  105. if (current instanceof EdgeUpdate) current = current.overwrites
  106. else break;
  107. }
  108. if (isReallyIncomingEdge) break;
  109. }
  110. if (!isReallyIncomingEdge) {
  111. throw new Error("Assertion failed: NodeDeletion in afterIncomingEdges does not delete an incoming edge.");
  112. }
  113. }
  114. else {
  115. if (supposedlyIncomingEdge.target.getTarget() === this.creation) {
  116. throw new Error("Assertion failed: Every element in afterIncomingEdges MUST set the target of the edge to SOME PLACE ELSE.");
  117. }
  118. if (![...supposedlyIncomingEdge.iterUpdates()].some(e => e.target.getTarget() === this.creation)) {
  119. throw new Error("Assertion failed: None of the EdgeUpdates of supposedlyIncomingEdge set the target to the node being deleted.");
  120. }
  121. }
  122. }
  123. this.description = "DEL("+this.creation.id.value.toString().slice(0,8)+")"
  124. // Detect conflicts
  125. // Delete/delete
  126. for (const concurrentDeletion of this.creation.deletions) {
  127. // Symmetric:
  128. this.deleteConflicts.push(concurrentDeletion);
  129. concurrentDeletion.deleteConflicts.push(this);
  130. }
  131. // Concurrently created outgoing edges of this node
  132. for (const outgoingEdgeCreation of this.creation.outgoingEdges) {
  133. if (!this.deletedOutgoingEdges.some(edge => edge.getCreation() === outgoingEdgeCreation)) {
  134. // Conflict: The deleted node has an outgoing edge that this deletion does not depend on.
  135. // Symmetric
  136. this.edgeSourceConflicts.push(outgoingEdgeCreation);
  137. outgoingEdgeCreation.deleteSourceConflicts.push(this);
  138. }
  139. }
  140. // Related to previous conflict type: Concurrent edge updates
  141. for (const deletedEdge of this.deletedOutgoingEdges) {
  142. for (const concurrentEdgeUpdate of deletedEdge.overwrittenBy) {
  143. if (this.afterIncomingEdges.includes(concurrentEdgeUpdate)) {
  144. // This is a special case that can occur when a node with a self-edge is deleted.
  145. // Not a conflict.
  146. }
  147. else {
  148. // Conflict: Edge concurrently updated and deleted.
  149. // Symmetric
  150. this.updateConflicts.push(concurrentEdgeUpdate);
  151. concurrentEdgeUpdate.updateConflicts.push(this);
  152. }
  153. }
  154. }
  155. function overwritesEdge(op: EdgeCreation | EdgeUpdate | NodeDeletion, edge: EdgeUpdate | EdgeCreation) {
  156. if (op === edge) {
  157. return true;
  158. }
  159. if (op instanceof EdgeUpdate) {
  160. return overwritesEdge(op.overwrites, edge);
  161. }
  162. if (op instanceof NodeDeletion) {
  163. return op.deletedOutgoingEdges.some(deletedEdge => overwritesEdge(deletedEdge, edge));
  164. }
  165. return false;
  166. }
  167. // Concurrently updated incoming edges of this node
  168. for (const incomingEdge of this.creation.incomingEdges) {
  169. // every incoming edge of deleted node must have been overwritten by an EdgeUpdate that is an explicit dependency:
  170. if (!this.afterIncomingEdges.some(edge => overwritesEdge(edge, incomingEdge))) {
  171. // Symmetric
  172. this.edgeTargetConflicts.push(incomingEdge);
  173. incomingEdge.target.addDeleteTargetConflict(this);
  174. }
  175. }
  176. // Create inverse dependencies
  177. this.creation.deletions.push(this);
  178. for (const deletedEdge of this.deletedOutgoingEdges) {
  179. // NodeDeletion acts a bit as an EdgeUpdate here
  180. deletedEdge.overwrittenBy.push(this);
  181. }
  182. }
  183. getDependencies(): Array<Delta> {
  184. return Array<Delta>().concat(
  185. [this.creation],
  186. this.deletedOutgoingEdges,
  187. this.afterIncomingEdges,
  188. );
  189. }
  190. getTypedDependencies(): Array<[Delta, string]> {
  191. return Array<[Delta, string]>().concat(
  192. [[this.creation, "DEL"]],
  193. this.deletedOutgoingEdges.map(edge => ([edge, "D"])),
  194. this.afterIncomingEdges.map(edge => ([edge, "A"])),
  195. );
  196. }
  197. getConflicts(): Array<Delta> {
  198. return Array<Delta>().concat(
  199. this.deleteConflicts,
  200. this.edgeSourceConflicts,
  201. this.edgeTargetConflicts,
  202. this.updateConflicts,
  203. );
  204. }
  205. getHash(): Buffer {
  206. return this.hash;
  207. }
  208. getDescription(): string {
  209. return this.description;
  210. }
  211. // pretty print to console under NodeJS
  212. [inspect.custom](depth: number, options: object) {
  213. return "NodeDeletion{" + inspect(this.creation.id, options) + ",delEdges=" + this.deletedOutgoingEdges.map(e => inspect(e, options)).join(",") + ",after=" + this.afterIncomingEdges.map(e => inspect(e, options)).join(",") + "}";
  214. }
  215. toString(): string {
  216. return this[inspect.custom](0, {});
  217. }
  218. serialize(): any {
  219. return {
  220. type: "NodeDeletion",
  221. creation: this.creation.hash.toString('base64'),
  222. deletedOutgoingEdges: this.deletedOutgoingEdges.map(d => d.hash.toString('base64')),
  223. afterIncomingEdges: this.afterIncomingEdges.map(d => d.hash.toString('base64')),
  224. };
  225. }
  226. *iterPrimitiveDeltas(): Iterable<Delta> {
  227. yield this;
  228. }
  229. }
  230. // Target of an edge can be: another node, nothing (edge doesn't exist) or a value (i.e., string, number or boolean)
  231. export type EdgeTargetType = NodeCreation | null | PrimitiveValue;
  232. // Target of an edge can be either: (1) another node, (2) a value or (3) null (hides the edge - initially all edges are assumed to be null).
  233. export interface SetsTarget {
  234. getTarget(): EdgeTargetType;
  235. addDeleteTargetConflict(nodeDeletion: NodeDeletion);
  236. getDeleteTargetConflicts(): ReadonlyArray<NodeDeletion>;
  237. getDependencies(): ReadonlyArray<NodeCreation>;
  238. getTypedDependencies(): ReadonlyArray<[NodeCreation, string]>;
  239. getHash(): Buffer;
  240. serialize(): any;
  241. }
  242. // Common functionality in EdgeCreation and EdgeUpdate: both set the target of an edge, and this can conflict with the deletion of the target.
  243. class SetsTargetToNode implements SetsTarget {
  244. // Dependency
  245. private readonly targetNode: NodeCreation;
  246. // Conflict: Concurrent deletion of target node.
  247. private deleteTargetConflicts: Array<NodeDeletion> = []; // append-only
  248. constructor(targetNode: NodeCreation, edgeOperation: EdgeCreation|EdgeUpdate) {
  249. this.targetNode = targetNode;
  250. // Concurrent deletion of target node
  251. if (this.targetNode instanceof NodeCreation) {
  252. for (const targetDeletion of this.targetNode.deletions) {
  253. if (targetDeletion.afterIncomingEdges.some(edge => {
  254. while (true) {
  255. if (edge === edgeOperation) return true;
  256. if (edge instanceof EdgeUpdate && edge.overwrites instanceof EdgeUpdate) edge = edge.overwrites;
  257. else return false;
  258. }
  259. })) {
  260. // this can never happen - something is very wrong if you get this error:
  261. throw new Error("Assertion failed - did not expect existing deletion to be aware of a new edge update");
  262. }
  263. // Symmetric
  264. this.deleteTargetConflicts.push(targetDeletion);
  265. targetDeletion.edgeTargetConflicts.push(edgeOperation);
  266. }
  267. // Create inverse dependency
  268. this.targetNode.incomingEdges.push(edgeOperation);
  269. }
  270. }
  271. getTarget(): EdgeTargetType {
  272. return this.targetNode;
  273. }
  274. addDeleteTargetConflict(nodeDeletion: NodeDeletion) {
  275. this.deleteTargetConflicts.push(nodeDeletion);
  276. }
  277. getDeleteTargetConflicts(): ReadonlyArray<NodeDeletion> {
  278. return this.deleteTargetConflicts;
  279. }
  280. getDependencies(): ReadonlyArray<NodeCreation> {
  281. return [this.targetNode];
  282. }
  283. getTypedDependencies(): ReadonlyArray<[NodeCreation, string]> {
  284. return [[this.targetNode, "TGT"]];
  285. }
  286. getHash(): Buffer {
  287. return this.targetNode.hash;
  288. }
  289. [inspect.custom](depth: number, options: object) {
  290. return inspect(this.targetNode.id, options);
  291. }
  292. serialize(): any {
  293. return {
  294. type: "node",
  295. creation: this.targetNode.hash.toString('base64'),
  296. }
  297. }
  298. }
  299. class SetsTargetToValue implements SetsTarget {
  300. readonly value: PrimitiveValue | null;
  301. constructor(value: PrimitiveValue | null) {
  302. this.value = value;
  303. }
  304. getTarget(): EdgeTargetType {
  305. return this.value;
  306. }
  307. addDeleteTargetConflict(nodeDeletion: NodeDeletion) {
  308. throw new Error("Assertion error: SetsTargetToValue cannot be involved in conflict with NodeDeletion");
  309. }
  310. getDeleteTargetConflicts(): ReadonlyArray<NodeDeletion> {
  311. return [];
  312. }
  313. getDependencies(): ReadonlyArray<NodeCreation> {
  314. return [];
  315. }
  316. getTypedDependencies(): ReadonlyArray<[NodeCreation, string]> {
  317. return [];
  318. }
  319. getHash(): Buffer {
  320. return Buffer.from(JSON.stringify(this.value));
  321. }
  322. [inspect.custom](depth: number, options: object) {
  323. return inspect(this.value, options);
  324. }
  325. serialize(): any {
  326. return {
  327. type: "value",
  328. value: this.value,
  329. }
  330. }
  331. }
  332. function makeSetsTarget(target: EdgeTargetType, edgeOperation: EdgeCreation|EdgeUpdate) {
  333. if (target instanceof NodeCreation) {
  334. return new SetsTargetToNode(target, edgeOperation);
  335. } else {
  336. return new SetsTargetToValue(target);
  337. }
  338. }
  339. export class EdgeCreation implements PrimitiveDelta {
  340. // Dependencies
  341. readonly source: NodeCreation;
  342. readonly label: string;
  343. readonly target: SetsTarget;
  344. readonly hash: Buffer;
  345. readonly description: string;
  346. // Inverse dependency
  347. // NodeDeletion if source of edge is deleted.
  348. overwrittenBy: Array<EdgeUpdate | NodeDeletion> = []; // append-only
  349. // Conflicts: Concurrent creations of the same edge.
  350. createConflicts: Array<EdgeCreation> = []; // append-only
  351. // Conflicts: Concurrent deletions of source node.
  352. deleteSourceConflicts: Array<NodeDeletion> = []; // append-only
  353. constructor(hash: Buffer, source: NodeCreation, label: string, target: EdgeTargetType) {
  354. this.hash = hash;
  355. this.source = source;
  356. this.label = label;
  357. this.target = makeSetsTarget(target, this);
  358. this.description = "EDG("+this.label+")";
  359. // Detect conflicts
  360. // Create/create
  361. for (const outgoingEdge of this.source.outgoingEdges) {
  362. if (outgoingEdge.label === this.label) {
  363. // Symmetric:
  364. this.createConflicts.push(outgoingEdge);
  365. outgoingEdge.createConflicts.push(this);
  366. }
  367. }
  368. // Concurrent deletions of source node
  369. for (const sourceDeletion of this.source.deletions) {
  370. if (sourceDeletion.deletedOutgoingEdges.some(edge => edge.getCreation() === this)) {
  371. // this can never happen - something is very wrong if you get this error:
  372. throw new Error("Assertion failed - did not expect existing deletion to be aware of a new edge creation");
  373. }
  374. // Symmetric
  375. this.deleteSourceConflicts.push(sourceDeletion);
  376. sourceDeletion.edgeSourceConflicts.push(this);
  377. }
  378. // Create inverse dependency
  379. this.source.outgoingEdges.push(this);
  380. }
  381. // Helper
  382. getCreation(): EdgeCreation {
  383. return this;
  384. }
  385. getDependencies(): Array<NodeCreation> {
  386. return [this.source, ...this.target.getDependencies()];
  387. }
  388. getTypedDependencies(): Array<[NodeCreation, string]> {
  389. return [[this.source, "SRC"], ...this.target.getTypedDependencies()];
  390. }
  391. getConflicts(): Array<Delta> {
  392. return Array<Delta>().concat(
  393. this.createConflicts,
  394. this.deleteSourceConflicts,
  395. this.target.getDeleteTargetConflicts(),
  396. );
  397. }
  398. getHash(): Buffer {
  399. return this.hash;
  400. }
  401. getDescription(): string {
  402. return this.description;
  403. }
  404. // pretty print to console under NodeJS
  405. [inspect.custom](depth: number, options: object) {
  406. return "EdgeCreation{src=" + inspect(this.source.id, options) + ",tgt=" + inspect(this.target, options) + ",label=" + this.label + "}";
  407. }
  408. toString(): string {
  409. return this[inspect.custom](0, {});
  410. }
  411. serialize(): any {
  412. return {
  413. type: "EdgeCreation",
  414. source: this.source.hash.toString('base64'),
  415. label: this.label,
  416. target: this.target.serialize(),
  417. };
  418. }
  419. *iterPrimitiveDeltas(): Iterable<Delta> {
  420. yield this;
  421. }
  422. }
  423. export class EdgeUpdate implements PrimitiveDelta {
  424. // Dependencies
  425. readonly overwrites: EdgeCreation | EdgeUpdate;
  426. readonly target: SetsTarget;
  427. readonly hash: Buffer;
  428. readonly description: string;
  429. // Inverse dependency
  430. // NodeDeletion if source of edge is deleted.
  431. overwrittenBy: Array<EdgeUpdate | NodeDeletion> = []; // append-only
  432. // Conflicts: Concurrent updates
  433. updateConflicts: Array<EdgeUpdate | NodeDeletion> = []; // append-only
  434. constructor(hash: Buffer, overwrites: EdgeCreation | EdgeUpdate, newTarget: EdgeTargetType) {
  435. this.hash = hash;
  436. this.overwrites = overwrites;
  437. this.target = makeSetsTarget(newTarget, this);
  438. this.description = "EDG";
  439. // Detect conflicts
  440. // Concurrent updates (by EdgeUpdate or NodeDeletion)
  441. for (const concurrentUpdate of this.overwrites.overwrittenBy) {
  442. // Symmetric
  443. this.updateConflicts.push(concurrentUpdate);
  444. concurrentUpdate.updateConflicts.push(this);
  445. }
  446. // Create inverse dependency
  447. this.overwrites.overwrittenBy.push(this);
  448. }
  449. // Helper
  450. getCreation(): EdgeCreation {
  451. return this.overwrites.getCreation();
  452. }
  453. getDependencies(): Array<Delta> {
  454. return [this.overwrites, ...this.target.getDependencies()];
  455. }
  456. getTypedDependencies(): Array<[Delta, string]> {
  457. return [[this.overwrites, "UPD"], ...this.target.getTypedDependencies()];
  458. }
  459. getConflicts(): Array<Delta> {
  460. return Array<Delta>().concat(
  461. this.updateConflicts,
  462. this.target.getDeleteTargetConflicts(),
  463. );
  464. }
  465. getHash(): Buffer {
  466. return this.hash;
  467. }
  468. getDescription(): string {
  469. return this.description;
  470. }
  471. // pretty print to console under NodeJS
  472. [inspect.custom](depth: number, options: object) {
  473. return "EdgeUpdate{upd=" + inspect(this.overwrites, options) + ",tgt=" + inspect(this.target, options) + "}";
  474. }
  475. toString(): string {
  476. return this[inspect.custom](0, {});
  477. }
  478. serialize(): any {
  479. return {
  480. type: "EdgeUpdate",
  481. overwrites: this.overwrites.hash.toString('base64'),
  482. target: this.target.serialize(),
  483. };
  484. }
  485. *iterUpdates() {
  486. let current: EdgeUpdate | EdgeCreation = this;
  487. while (true) {
  488. yield current;
  489. if (current instanceof EdgeUpdate) {
  490. current = current.overwrites;
  491. } else {
  492. return;
  493. }
  494. }
  495. }
  496. *iterPrimitiveDeltas(): Iterable<Delta> {
  497. yield this;
  498. }
  499. }
  500. function targetToHash(target: EdgeTargetType): any {
  501. // just needs to return something "unique" to feed into the hash function
  502. if (target instanceof NodeCreation) {
  503. return target.hash;
  504. }
  505. else {
  506. return "value" + JSON.stringify(target);
  507. }
  508. }
  509. // Ensures that deltas with the same hash are only created once.
  510. export class PrimitiveRegistry {
  511. deltas: Map<string, PrimitiveDelta> = new Map();
  512. private createIdempotent(hash: Buffer, callback) {
  513. const base64 = hash.toString('base64');
  514. return this.deltas.get(base64) || (() => {
  515. const delta = callback();
  516. this.deltas.set(base64, delta);
  517. return delta;
  518. })();
  519. }
  520. newNodeCreation(id: UUID): NodeCreation {
  521. const hash = createHash('sha256')
  522. .update(JSON.stringify(id.value)) // prevent collisions between 'true' (actual boolean) and '"true"' (string "true"), or 42 (number) and "42" (string)
  523. .digest();
  524. return this.createIdempotent(hash, () => new NodeCreation(hash, id));
  525. }
  526. newNodeDeletion(creation: NodeCreation, deletedOutgoingEdges: Array<EdgeCreation|EdgeUpdate>, afterIncomingEdges: Array<EdgeUpdate|NodeDeletion>): NodeDeletion {
  527. // hash will be calculated based on all EdgeCreations/EdgeUpdates/NodeDeletions that we depend on, by XOR (insensitive to array order).
  528. let hash = createHash('sha256').update(creation.hash);
  529. let union = Buffer.alloc(32); // all zeroes - neutral element for XOR
  530. for (const deletedOutgoing of deletedOutgoingEdges) {
  531. union = bufferXOR(union, deletedOutgoing.getHash());
  532. }
  533. for (const afterIncoming of afterIncomingEdges) {
  534. union = bufferXOR(union, afterIncoming.getHash());
  535. }
  536. const buf = hash.update(union).digest();
  537. return this.createIdempotent(buf, () => new NodeDeletion(buf, creation, deletedOutgoingEdges, afterIncomingEdges));
  538. }
  539. newEdgeCreation(source: NodeCreation, label: string, target: EdgeTargetType): EdgeCreation {
  540. const hash = createHash('sha256')
  541. .update(source.hash)
  542. .update('create').update(label)
  543. .update('target=').update(targetToHash(target))
  544. .digest();
  545. return this.createIdempotent(hash, () => new EdgeCreation(hash, source, label, target));
  546. }
  547. newEdgeUpdate(overwrites: EdgeCreation | EdgeUpdate, target: EdgeTargetType): EdgeUpdate {
  548. const hash = createHash('sha256')
  549. .update(overwrites.hash)
  550. .update('target=').update(targetToHash(target))
  551. .digest();
  552. return this.createIdempotent(hash, () => new EdgeUpdate(hash, overwrites, target));
  553. }
  554. }