test.js 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269
  1. "use strict";
  2. // Should work in browser but only tested with NodeJS v14.16.1
  3. const { Context, History } = require("./doh.js");
  4. // From: https://stackoverflow.com/a/43260158
  5. // returns all the permutations of a given array
  6. function perm(xs) {
  7. let ret = [];
  8. for (let i = 0; i < xs.length; i = i + 1) {
  9. let rest = perm(xs.slice(0, i).concat(xs.slice(i + 1)));
  10. if(!rest.length) {
  11. ret.push([xs[i]])
  12. } else {
  13. for(let j = 0; j < rest.length; j = j + 1) {
  14. ret.push([xs[i]].concat(rest[j]))
  15. }
  16. }
  17. }
  18. return ret;
  19. }
  20. // Reinventing the wheel:
  21. class AssertionError extends Error {
  22. constructor(msg) {
  23. super(msg);
  24. }
  25. }
  26. function assert(expr, msg) {
  27. if (!expr) {
  28. // console.log(...arguments);
  29. throw new AssertionError(msg);
  30. }
  31. }
  32. function deepEqual(val1, val2) {
  33. if (typeof(val1) !== typeof(val2)) return false;
  34. if ((val1 === null) !== (val2 === null)) return false;
  35. switch (typeof(val1)) {
  36. case 'object':
  37. for (var p in val2) {
  38. if (val1[p] === undefined) return false;
  39. }
  40. for (var p in val1) {
  41. if (!deepEqual(val1[p], val2[p])) return false;
  42. }
  43. return true;
  44. case 'array':
  45. if (val1.length !== val2.length) return false;
  46. for (let i=0; i<val1.length; ++i)
  47. if (!deepEqual(val1[i], val2[i])) return false;
  48. return true;
  49. default:
  50. return val1 === val2;
  51. }
  52. }
  53. // Test:
  54. async function runTest(verbose) {
  55. function info() {
  56. if (verbose) console.log(...arguments);
  57. }
  58. function resolve(op1, op2) {
  59. // info("resolve...", props1, props2)
  60. if (op1.detail.get('geometry').value !== op2.detail.get('geometry').value) {
  61. return op1.detail.get('geometry').value > op2.detail.get('geometry').value;
  62. }
  63. return op1.detail.get('style').value > op2.detail.get('style').value;
  64. }
  65. function createAppState(label) {
  66. const state = {};
  67. function setState(prop, val) {
  68. state[prop] = val;
  69. info(" ", label, "state =", state);
  70. }
  71. return {setState, state};
  72. }
  73. function createHistory(label, context) {
  74. const {setState, state} = createAppState(label);
  75. // const context = new Context(requestCallback); // simulate 'remoteness' by creating a new context for every History.
  76. const history = new History(context, setState, resolve);
  77. return {history, state};
  78. }
  79. {
  80. info("\nTest case: Add local operations (no concurrency) in random order.\n")
  81. const local = new Context();
  82. info("insertions...")
  83. const {history: expectedHistory, state: expectedState} = createHistory("expected", local);
  84. const insertions = [
  85. /* 0: */ expectedHistory.new({geometry: 1, style: 1}),
  86. /* 1: */ expectedHistory.new({geometry: 2}), // depends on 0
  87. /* 2: */ expectedHistory.new({style: 2}), // depends on 0
  88. ];
  89. const permutations = perm(insertions);
  90. for (const insertionOrder of permutations) {
  91. info("permutation...")
  92. const {history: actualHistory, state: actualState} = createHistory("actual", local);
  93. // Sequential
  94. for (const op of insertionOrder) {
  95. actualHistory.autoMerge(op);
  96. }
  97. console.log("expected:", expectedState, "actual:", actualState)
  98. assert(deepEqual(expectedState, actualState));
  99. }
  100. }
  101. function noFetch() {
  102. throw new AssertionError("Did not expect fetch");
  103. }
  104. {
  105. info("\nTest case: Multi-user without conflict\n")
  106. // Local and remote are just names for our histories.
  107. const localContext = new Context(noFetch);
  108. const remoteContext = new Context(noFetch);
  109. const {history: localHistory, state: localState } = createHistory("local", localContext);
  110. const {history: remoteHistory, state: remoteState} = createHistory("remote", remoteContext);
  111. const localOp1 = localHistory.new({geometry: 1});
  112. await remoteHistory.receiveAndMerge(localOp1.serialize());
  113. console.log("11")
  114. const remoteOp2 = remoteHistory.new({geometry: 2}); // happens after (hence, overwrites) op1
  115. await localHistory.receiveAndMerge(remoteOp2.serialize());
  116. assert(deepEqual(localState, remoteState));
  117. }
  118. {
  119. info("\nTest case: Concurrency with conflict\n")
  120. const localContext = new Context(noFetch);
  121. const remoteContext = new Context(noFetch);
  122. const {history: localHistory, state: localState} = createHistory("local", localContext);
  123. const {history: remoteHistory, state: remoteState} = createHistory("remote", remoteContext);
  124. const localOp1 = localHistory.new({geometry: 1});
  125. const remoteOp2 = remoteHistory.new({geometry: 2});
  126. await localHistory.receiveAndMerge(remoteOp2.serialize());
  127. await remoteHistory.receiveAndMerge(localOp1.serialize());
  128. assert(deepEqual(localState, remoteState));
  129. }
  130. {
  131. info("\nTest case: Concurrency with conflict (2)\n")
  132. const localContext = new Context(noFetch);
  133. const remoteContext = new Context(noFetch);
  134. const {history: localHistory, state: localState} = createHistory("local", localContext);
  135. const {history: remoteHistory, state: remoteState} = createHistory("remote", remoteContext);
  136. info("localHistory insert...")
  137. const localOp1 = localHistory.new({geometry: 1});
  138. const localOp2 = localHistory.new({geometry: 4});
  139. info("remoteHistory insert...")
  140. const remoteOp3 = remoteHistory.new({geometry: 2});
  141. const remoteOp4 = remoteHistory.new({geometry: 3});
  142. info("localHistory receive...")
  143. await localHistory.receiveAndMerge(remoteOp3.serialize()); // op3 wins from op1 -> op2 and op1 undone
  144. await localHistory.receiveAndMerge(remoteOp4.serialize()); // buffered
  145. info("remoteHistory receive...")
  146. await remoteHistory.receiveAndMerge(((localOp1.serialize()))); // op1 loses from op3
  147. await remoteHistory.receiveAndMerge(((localOp2.serialize()))); // no conflict
  148. assert(deepEqual(localState, remoteState));
  149. }
  150. {
  151. info("\nTest case: Fetch\n")
  152. const fetched = [];
  153. async function fetchFromLocal(id) {
  154. // console.log("fetching", id)
  155. fetched.push(id);
  156. return localContext.ops.get(id).then(op => op.serialize());
  157. }
  158. const localContext = new Context(noFetch);
  159. const remoteContext = new Context(fetchFromLocal);
  160. const {history: localHistory, state: localState} = createHistory("local", localContext);
  161. const localOps = [
  162. localHistory.new({geometry:1}), // [0] (no deps)
  163. localHistory.new({geometry:2, style: 3}), // [1], depends on [0]
  164. localHistory.new({style: 4}), // [2], depends on [1]
  165. localHistory.new({geometry: 5, style: 6, parent: 7}), // [3], depends on [1], [2]
  166. localHistory.new({parent: 8}), // [4], depends on [3]
  167. localHistory.new({terminal: 9}), // [5] (no deps)
  168. ];
  169. // when given [2], should fetch [1], then [0]
  170. await remoteContext.receiveOperation(localOps[2].serialize());
  171. assert(deepEqual(fetched, [localOps[1].id, localOps[0].id]));
  172. // when given [5], should not fetch anything
  173. await remoteContext.receiveOperation(localOps[5].serialize());
  174. assert(deepEqual(fetched, [localOps[1].id, localOps[0].id]));
  175. // when given [4], should fetch [3]. (already have [0-2] from previous step)
  176. await remoteContext.receiveOperation(localOps[4].serialize());
  177. assert(deepEqual(fetched, [localOps[1].id, localOps[0].id, localOps[3].id]));
  178. }
  179. {
  180. info("\nTest case: Get as sequence\n")
  181. const {history} = createHistory("local", new Context(noFetch));
  182. const ops = [
  183. history.new({x:1, y:1}), // 0
  184. history.new({x:2}), // 1 depends on 0
  185. history.new({y:2}), // 2 depends on 0
  186. history.new({x:3, z:3}), // 3 depends on 1
  187. history.new({a:4}), // 4
  188. history.new({a:5}), // 5 depends on 4
  189. history.new({a:6, z:6}), // 6 depends on 5, 3
  190. ];
  191. const seq = history.getOpsSequence();
  192. console.log(seq.map(op => op.serialize()));
  193. assert(seq.indexOf(ops[1]) > seq.indexOf(0));
  194. assert(seq.indexOf(ops[2]) > seq.indexOf(0));
  195. assert(seq.indexOf(ops[3]) > seq.indexOf(1));
  196. assert(seq.indexOf(ops[5]) > seq.indexOf(4));
  197. assert(seq.indexOf(ops[6]) > seq.indexOf(5));
  198. assert(seq.indexOf(ops[6]) > seq.indexOf(3));
  199. }
  200. }
  201. runTest(/* verbose: */ true).then(() => {
  202. console.log("OK");
  203. }, err => {
  204. console.log(err);
  205. process.exit(1);
  206. });