author | Alan Dipert
<alan@dipert.org> 2021-06-11 06:03:29 UTC |
committer | Alan Dipert
<alan@dipert.org> 2021-06-11 06:03:29 UTC |
parent | 9c5b320c10d3761e11a8d1f75a5463e98337a769 |
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()); });