git » hoplite.git » commit b227a47

entities works, simplify todo to hoplon

author Alan Dipert
2021-06-11 06:03:29 UTC
committer Alan Dipert
2021-06-11 06:03:29 UTC
parent 9c5b320c10d3761e11a8d1f75a5463e98337a769

entities works, simplify todo to hoplon

el.mjs +87 -138
reactives.mjs +50 -3
todo.mjs +27 -41

diff --git a/el.mjs b/el.mjs
index 93503ba..c2f9985 100644
--- a/el.mjs
+++ b/el.mjs
@@ -1,12 +1,13 @@
 import { _ } from './datalog.mjs';
-import { Input, Formula, input, formula, database, view, watch } from './reactives.mjs';
+import { Input, Formula, input, formula, database, view, watch, captureCreated } from './reactives.mjs';
 
 function el(tag, attrs, kids) {
   let el = document.createElement(tag);
   for (let [k, v] of Object.entries(attrs)) {
     if (v instanceof Input || v instanceof Formula) {
       watch((oldv, newv) => {
-        el.setAttribute(k, newv);
+        el[k] = newv;
+        // el.setAttribute(k, newv);
       })(v);
     } else if (k.startsWith("on")) {
       el.addEventListener(k.slice(2), v);
@@ -76,114 +77,131 @@ function makeAttrCells(db, eidCell) {
   })
 }
 
-function insertSorted(el, nodeToAttrs, sortAttr, node) {
-  if (!el.children.length) {
-    el.appendChild(node);
-  } else if (nodeToAttrs.get(el.children[el.children.length-1])[sortAttr].value <= nodeToAttrs.get(node)[sortAttr].value){
+function getComparator(name) {
+  if (name === "asc") {
+    return (a, b) => a === b ? 0 : a < b ? -1 : 1;
+  } else if (name === "desc") {
+    return (a, b) => a === b ? 0 : b < a ? -1 : 1;
+  } else {
+    throw new Error(`Unknown comparator: ${name}`);
+  }
+}
+
+function splice(el, i, node) {
+  if (el.children.length === 0 || el.children.length === i) {
     el.appendChild(node);
   } else {
-    for (let i = 0; i < el.children.length; i++) {
-      if (nodeToAttrs.get(el.children[i])[sortAttr].value >= nodeToAttrs.get(node)[sortAttr].value) {
-        el.insertBefore(node, el.children[i]);
-        break;
-      }
+    el.insertBefore(node, el.children[i]);
+  }
+}
+
+function binaryInsert(el, nodeToAttrs, sortAttr, node, cmpFunc) {
+  let left = 0,
+      right = el.children.length,
+      middle,
+      cmp;
+  while (left < right) {
+    middle = (left + right) >> 1;
+    cmp = cmpFunc(
+      nodeToAttrs.get(node)[sortAttr].value,
+      nodeToAttrs.get(el.children[middle])[sortAttr].value
+    );
+    if (cmp > 0) {
+      left = middle + 1;
+    } else if (cmp < 0) {
+      right = middle;
+    } else if (cmp === 0) {
+      left++;
+      break;
+    } else {
+      throw new Error(`Bad compare value: ${cmp}`);
     }
   }
+  splice(el, left, node)
+}
+
+function asCell(val, defaultVal) {
+  if (val instanceof Input || val instanceof Formula) {
+    return val;
+  } else if (val === undefined) {
+    return input(defaultVal)
+  } else {
+    return input(val);
+  }
 }
 
 class EntityNodes {
-  constructor(db, makeNode, sortAttr = input("_eid")) {
+  constructor(db, sortAttr, sortDir, makeNode) {
     this.db = db;
+    this.sortAttr = asCell(sortAttr, "_eid");
+    this.sortDir = asCell(sortDir, "asc");
     this.makeNode = makeNode;
-    this.sortAttr = sortAttr;
   }
   bind(el) {
     let eidToNode = new Map(),
         nodeToEidCell = new Map(),
         nodeToAttrs = new Map(),
-        eidToInsertFormula = new Map(),
+        nodeToReactives = new Map(),
+        eidToInserter = new Map(),
         pool = [];
+    window.pool = pool;
     watch((added, removed) => {
       let node;
       for (let [eid] of removed) {
         node = eidToNode.get(eid);
+        for (let r of nodeToReactives.get(node)) {
+          r.pause();
+        }
         el.removeChild(node);
         eidToNode.delete(eid);
-        eidToInsertFormula.get(eid).detach();
-        eidToInsertFormula.delete(eid);
+        eidToInserter.get(eid).detach();
+        eidToInserter.delete(eid);
         pool.push(node);
       }
       for (let [eid] of added) {
         if (pool.length) {
           node = pool.pop();
+          for (let r of nodeToReactives.get(node)) {
+            r.resume();
+          }
           nodeToEidCell.get(node).set(eid);
           eidToNode.set(eid, node);
         } else {
           let eidCell = input(eid),
-              attrs = makeAttrCells(this.db, eidCell);
-          node = this.makeNode(attrs);
+              attrs = makeAttrCells(this.db, eidCell),
+              reactives = captureCreated(() => {
+                node = this.makeNode(attrs);
+              });
+          nodeToReactives.set(node, reactives);
           eidToNode.set(eid, node);
           nodeToEidCell.set(node, eidCell);
           nodeToAttrs.set(node, attrs);
         }
-        eidToInsertFormula.set(eid, null);
+        // eidToInserter.set(eid, watch(() => {
+        //   binaryInsert(el, nodeToAttrs, this.sortAttr.value, node, getComparator(this.sortDir.value));
+        // })(nodeToAttrs.get(node)[this.sortAttr.value]));
+        eidToInserter.set(eid, watch(() => {
+          binaryInsert(el, nodeToAttrs, this.sortAttr.value, node, getComparator(this.sortDir.value));
+        })(nodeToAttrs.get(node)[this.sortAttr.value]));
       }
     })(view({find: [_.eid], where: [[_.eid]]})(this.db));
-    watch((oldv, newv) => {
-      for (let [eid, f] of [...eidToInsertFormula]) {
-        if (el.contains(eidToNode.get(eid))) {
-          el.removeChild(eidToNode.get(eid));
-        }
+    formula((sortAttr, sortDir) => {
+      while (el.children.length) {
+        el.removeChild(el.children[0]);
+      }
+      for (let [eid, f] of [...eidToInserter]) {
+        let node = eidToNode.get(eid);
         f?.detach();
-        eidToInsertFormula.set(eid, formula(attr => {
-          insertSorted(el, nodeToAttrs, newv, eidToNode.get(eid));
-        })(nodeToAttrs.get(eidToNode.get(eid))[newv]));
+        eidToInserter.set(eid, watch(() => {
+          binaryInsert(el, nodeToAttrs, sortAttr, node, getComparator(sortDir));
+        })(nodeToAttrs.get(node)[sortAttr]));
       }
-    })(this.sortAttr);
+    })(this.sortAttr, this.sortDir);
   }
 }
 
-// class EntityNodes {
-//   constructor(db, makeNode) {
-//     this.db = db;
-//     this.makeNode = makeNode;
-//     this.pool = [];
-//     this.nodeToEidCell = new Map();
-//     this.eidToNode = new Map();
-//     this.bufferedCommands = [];
-//     this.callback = command => this.bufferedCommands.push(command);
-//     watch((added, removed) => {
-//       for (let [eid] of removed) {
-//         let node = this.eidToNode.get(eid);
-//         this.eidToNode.delete(eid);
-//         this.callback([Node.prototype.removeChild, node]);
-//       }
-//       for (let [eid] of added) {
-//         let node;
-//         if (this.pool.length) {
-//           node = this.pool.pop();
-//           nodeToEidCell.get(node).set(eid);
-//         } else {
-//           let eidCell = input(eid),
-//               attrs = makeAttrCells(db, eidCell);
-//           node = this.makeNode(attrs);
-//           this.eidToNode.set(eid, node);
-//           this.nodeToEidCell.set(node, eidCell);
-//         }
-//         this.callback([Node.prototype.appendChild, node]);
-//       }
-//     })(view({find: [_.eid], where: [[_.eid]]})(db))
-//   }
-//   setCallback(f) {
-//     this.callback = f;
-//     while (this.bufferedCommands.length) {
-//       this.callback(this.bufferedCommands.pop());
-//     }
-//   }
-// }
-
-function entities(db, makeNode) {
-  return new EntityNodes(db, makeNode, input("_eid"));
+function entities(db, sortAttr, sortDir, makeNode) {
+  return new EntityNodes(db, sortAttr, sortDir, makeNode);
 }
 
 function entitiesBy(db, sortAttr, makeNode) {
@@ -194,73 +212,4 @@ function entitiesBy(db, sortAttr, makeNode) {
   return new EntityNodes(db, makeNode, sortAttr)
 }
 
-// function mapEntities(parent, db, eidView, attrs, makeChild) {
-//   let pool = [],
-//       childEids = new Map();
-//   watch((added, removed) => {
-//     for (let [eid] of removed) {
-//       let child = [...childEids].filter(([k, v]) => v.value === eid)[0][0];
-//       parent.removeChild(child);
-//     }
-//     for (let [eid] of added) {
-//       let child;
-//       if (pool.length) {
-//         child = pool.pop();
-//         childEids.get(child).set(eid);
-//       } else {
-//         let eidCell = input(eid),
-//             attrCells = {eid: eidCell};
-//         for (let attr of attrs) {
-//           attrCells[attr] = input(null);
-//           watch((added, removed) => {
-//             if (added.size()) {
-//               let [[newv]] = added;
-//               attrCells[attr].set(newv);
-//             } else if (removed.size()) {
-//               attrCells[attr].set("");
-//             }
-//           })(view({
-//             find: [_.attrVal],
-//             use: [_.eid],
-//             where: [[_.eid, attr, _.attrVal]]
-//           })(db, eidCell))
-//         }
-//         child = makeChild(attrCells);
-//         childEids.set(child, eidCell);
-//       }
-//       parent.appendChild(child);
-//     }
-//   })(eidView);
-//   return parent;
-// }
-
-// function renderChildren(parent, eidView, klass) {
-//   let children = new Map();
-//   watch((added, removed) => {
-//     for (let [eid] of removed) {
-//       parent.removeChild(children.get(eid).el);
-//       children.get(eid).destroy();
-//       children.delete(eid);
-//     }
-//     for (let [eid] of added) {
-//       children.set(eid, new klass(eid));
-//       parent.appendChild(children.get(eid).el)
-//     }
-//   })(eidView);
-//   return parent;
-// }
-
 export { $el, entities, entitiesBy };
-// function renderInto(parent, view, templateFunction) {
-// }
-
-// renderInto($el.ol, view({
-//   find: [_.id],
-//   where: [[_.id _._ _._]]
-// }, todos, ([idCell]) => $.li(
-//   {class: "todo"},
-//   view({
-//     find: [_.text],
-//     use: [_.id],
-//   })
-// )))
diff --git a/reactives.mjs b/reactives.mjs
index 06d7280..e9a444c 100644
--- a/reactives.mjs
+++ b/reactives.mjs
@@ -1,8 +1,10 @@
 import { StupidTupleSet, partialQuery } from './datalog.mjs';
 
 const depGraph = new Map(),
-      suspended = new Set();
-let inTransaction = false;
+      suspended = new Set(),
+      captured = new Set();
+let inTransaction = false,
+    capturing = false;
 
 function addEdge(g, from, to) {
   let s = g.has(from) ? g.get(from) : new Set();
@@ -32,6 +34,22 @@ function propagate(g, from, walked = new Set()) {
   });
 }
 
+function captureCreated(thunk) {
+  if (capturing) {
+    thunk();
+  } else {
+    try {
+      capturing = true;
+      thunk();
+    } finally {
+      capturing = false;
+      let capturedCopy = new Set([...captured]);
+      captured.clear();
+      return capturedCopy;
+    }
+  }
+}
+
 function transaction(thunk) {
   if (inTransaction) {
     thunk();
@@ -78,8 +96,18 @@ class Formula {
       .filter(Formula.isReactiveSource)
       .forEach(source => addEdge(depGraph, source, this));
     this.value = this.previousValue = undefined;
+    this.paused = false;
+    if (capturing) {
+      captured.add(this);
+    }
     this.update();
   }
+  pause() {
+    this.paused = true;
+  }
+  resume() {
+    this.paused = false;
+  }
   static isReactiveSource(x) {
     return x instanceof Input || x instanceof Formula;
   }
@@ -90,6 +118,7 @@ class Formula {
     return this.sources.map(x => Formula.isReactiveSource(x) ? x.value : x);
   }
   update() {
+    if (this.paused) return false;
     let value = this.f.apply(null, this.sourceValues());
     if (this.value !== value) {
       this.previousValue = this.value;
@@ -105,6 +134,10 @@ class Database {
     this.set = new StupidTupleSet();
     this.added = new StupidTupleSet();
     this.removed = new StupidTupleSet();
+    this.maxEid = 0;
+  }
+  nextEid() {
+    return this.maxEid + 1;
   }
   add(tuples) {
     let changed = false;
@@ -114,6 +147,9 @@ class Database {
         this.added.add(tuple);
         this.removed.remove(tuple);
         this.set.add(tuple);
+        if (tuple.length > 0 && tuple[0] > this.maxEid) {
+          this.maxEid = tuple[0];
+        }
       }
     }
     if (changed) {
@@ -167,8 +203,18 @@ class View {
     sources
       .filter(View.isReactiveSource)
       .forEach(source => addEdge(depGraph, source, this));
+    this.paused = false;
+    if (capturing) {
+      captured.add(this);
+    }
     this.update();
   }
+  pause() {
+    this.paused = true;
+  }
+  resume() {
+    this.paused = false;
+  }
   detach() {
     this.sources
       .filter(View.isReactiveSource)
@@ -192,6 +238,7 @@ class View {
     });
   }
   update() {
+    if (this.paused) return false;
     let newSet = this.query(...this.sourceValues()),
         added = newSet.complement(this.set),
         removed = this.set.complement(newSet);
@@ -268,7 +315,7 @@ function watch(f) {
   return (source) => new Watch(f, source);
 }
 
-export { Input, Formula, transaction, input, formula, database, view, watch };
+export { Input, Formula, transaction, captureCreated, input, formula, database, view, watch };
 
 // let A = input(100),
 //     B = input(200),
diff --git a/todo.mjs b/todo.mjs
index cd55cf9..34f02ea 100644
--- a/todo.mjs
+++ b/todo.mjs
@@ -1,47 +1,33 @@
-import { query, _, ANY, paths } from './datalog.mjs';
-import { input, formula, database, view } from './reactives.mjs';
-import { $el, entities, entitiesBy } from './el.mjs';
+import { input, database, transaction } from './reactives.mjs';
+import { $el, entities } from './el.mjs';
 
-let todos = database([
-  [0, "text", "do the thing"],
-  [0, "status", "ready"],
-  [1, "text", "do the other thing"],
-  [1, "status", "ready"],
-  [2, "text", "do that last thing"],
-  [2, "status", "done"]
-]);
-
-let showStatus = input(ANY.VALUE);
-
-let visibleTodos = view({
-  find: [_.e, _.a, _.v],
-  use: [_.status],
-  where: [
-    [_.e, "status", _.status],
-    [_.e, _.a, _.v]
-  ]
-})(todos, showStatus);
-
-window.by = input("_eid");
-
-let todoList = $el.div(
-  $el.select({
-    onchange: ({target: {value}}) => {
-      showStatus.set(value === "any" ? ANY.VALUE : value);
-    }
-  },
-    $el.option({value: "any"}, "Any"),
-    $el.option({value: "ready"}, "Ready"),
-    $el.option({value: "done"}, "Done")
-  ),
-  $el.ul(
-    entitiesBy(visibleTodos, window.by, attrs => $el.li(
-      formula((eid, status, text) => `${eid} - ${status} - ${text}`)(attrs._eid, attrs.status, attrs.text)
-    ))
+function todoList() {
+  let todos = database([]),
+      newTodo = input("");
+  return $el.div(
+    $el.h3("TODO"),
+    $el.input({
+      type: "text",
+      value: newTodo,
+      onchange: ({target: {value}}) => newTodo.set(value)
+    }),
+    $el.input({
+      type: "button",
+      value: "Add",
+      onclick: () => transaction(() => {
+        todos.add([
+          [todos.maxEid + 1, "text", newTodo.value]
+        ]);
+        newTodo.set("");
+      })
+    }),
+    $el.ul(
+      entities(todos, "_eid", "asc", attrs => $el.li(attrs.text))
+    )
   )
-);
+}
 
 document.addEventListener("DOMContentLoaded", () => {
-  document.body.appendChild(todoList);
+  document.body.appendChild(todoList());
 });