author | Alan Dipert
<alan@dipert.org> 2021-05-29 05:11:17 UTC |
committer | Alan Dipert
<alan@dipert.org> 2021-05-29 05:11:17 UTC |
parent | 959dfd9ec5f585991c3343db52b7b93905ca20a0 |
datalog.mjs | +30 | -20 |
el.mjs | +105 | -36 |
reactives.mjs | +8 | -8 |
tests.mjs | +14 | -5 |
todo.mjs | +81 | -51 |
diff --git a/datalog.mjs b/datalog.mjs index 7e77a70..cdd4744 100644 --- a/datalog.mjs +++ b/datalog.mjs @@ -13,8 +13,6 @@ const ANY = { }; function match(clause, fact) { - if (clause.length !== fact.length) - return; let matches = {}; for (let i = 0; i < clause.length; i++) { if (isVar(clause[i])) { @@ -33,39 +31,51 @@ function match(clause, fact) { return matches; } -class JSONSet { - constructor(values = []) { - this.values = {}; - values.forEach(value => this.add(value)); +function tupleEqual(x, y) { + if (x.length !== y.length) + return false; + for (let i = 0; i < x.length; i++) { + if (x[i] !== y[i]) + return false; } - add(value) { - this.values[JSON.stringify(value)] = value; + return true; +} + +class TupleSet { + constructor(tuples = []) { + this.tuples = []; + tuples.forEach(tuple => this.add(tuple)); + } + add(tuple) { + if (!this.has(tuple)) { + this.tuples.push(tuple); + } } - remove(value) { - if (!this.has(value)) { + remove(tuple) { + if (!this.has(tuple)) { return false; } else { - delete this.values[JSON.stringify(value)]; + this.tuples = this.tuples.filter(v => !tupleEqual(v, tuple)); return true; } } [Symbol.iterator]() { - return Object.values(this.values)[Symbol.iterator](); + return this.tuples[Symbol.iterator](); } - has(value) { - return JSON.stringify(value) in this.values; + has(tuple) { + return this.tuples.some(v => tupleEqual(v, tuple)); } clear() { - this.values = {}; + this.tuples = []; } complement(other) { - return new JSONSet([...this].filter(value => !other.has(value))); + return new TupleSet([...this].filter(tuple => !other.has(tuple))); } size() { - return Object.keys(this.values).length; + return this.tuples.length; } toArray() { - return [...this]; + return [...this.tuples]; } } @@ -147,7 +157,7 @@ function initRel(use, vals) { } function select(rel, vars) { - return new JSONSet([...rel].map(rx => vars.map(v => rx[v]))); + return new TupleSet([...rel].map(rx => vars.map(v => rx[v]))); } function parseVals(use, vals) { @@ -203,7 +213,7 @@ function partialQuery(q) { return (...vals) => query(q, ...vals); } -export { _, query, JSONSet, partialQuery }; +export { _, query, TupleSet, partialQuery }; // let db = new JSONSet([ // ["sally", "age", 21], // ["fred", "age", 42], diff --git a/el.mjs b/el.mjs index 2089485..c1a622e 100644 --- a/el.mjs +++ b/el.mjs @@ -26,6 +26,10 @@ function el(tag, attrs, kids) { textNode.textContent = newv; })(kid); el.appendChild(textNode); + } else if (kid instanceof EntityNodes) { + kid.setCallback(([func, ...args]) => { + func.apply(el, args); + }) } else { el.appendChild(kid); } @@ -38,7 +42,8 @@ const $el = new Proxy({}, { if (typeof attrs === "string" || attrs instanceof Node || attrs instanceof Input - || attrs instanceof Formula) { + || attrs instanceof Formula + || attrs instanceof EntityNodes) { kids.unshift(attrs); attrs = {}; } @@ -48,46 +53,110 @@ const $el = new Proxy({}, { } }); -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); +function makeAttrCells(db, eidCell) { + let cellCache = new Map(); + return new Proxy({}, { + get: (obj, attrName) => { + if (cellCache.has(name)) + return cellCache.get(name); + let attrValCell = input(null); + watch((added, removed) => { + if (added.size()) { + let [[newv]] = added; + attrValCell.set(newv); + } else if (removed.size()) { + attrValCell.set(null); + } + })(view({ + find: [_.attrVal], + use: [_.eid], + where: [[_.eid, attrName, _.attrVal]] + })(db, eidCell)); + cellCache.set(attrName, attrValCell); + return attrValCell; } - 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)) + }) +} + +class EntityNodes { + constructor(db, makeNode) { + this.db = db; + this.makeNode = makeNode; + this.pool = []; + this.nodeToEidCell = new Map(); + this.bufferedCommands = []; + this.callback = command => this.bufferedCommands.push(command); + watch((added, removed) => { + for (let [eid] of removed) { + // TODO + } + 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.nodeToEidCell.set(node, eidCell); } - child = makeChild(attrCells); - childEids.set(child, eidCell); + this.callback([Node.prototype.appendChild, node]); } - parent.appendChild(child); + })(view({find: [_.eid], where: [[_.eid]]})(db)) + } + setCallback(f) { + this.callback = f; + while (this.bufferedCommands.length) { + this.callback(this.bufferedCommands.pop()); } - })(eidView); - return parent; + } +} + +function entities(db, makeNode) { + return new EntityNodes(db, makeNode); } +// 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) => { @@ -104,7 +173,7 @@ function mapEntities(parent, db, eidView, attrs, makeChild) { // return parent; // } -export { $el, on, mapEntities }; +export { $el, on, entities }; // function renderInto(parent, view, templateFunction) { // } diff --git a/reactives.mjs b/reactives.mjs index 4ed91b5..a2d4eb1 100644 --- a/reactives.mjs +++ b/reactives.mjs @@ -1,4 +1,4 @@ -import { JSONSet, partialQuery } from './datalog.mjs'; +import { TupleSet, partialQuery } from './datalog.mjs'; const depGraph = new Map(), suspended = new Set(); @@ -99,9 +99,9 @@ class Formula { class Database { constructor() { - this.set = new JSONSet(); - this.added = new JSONSet(); - this.removed = new JSONSet(); + this.set = new TupleSet(); + this.added = new TupleSet(); + this.removed = new TupleSet(); } add(tuples) { let changed = false; @@ -157,9 +157,9 @@ class View { // q is not reactive (for now) this.query = q instanceof Function ? q : partialQuery(q); this.sources = sources; - this.set = new JSONSet(); - this.added = new JSONSet(); - this.removed = new JSONSet(); + this.set = new TupleSet(); + this.added = new TupleSet(); + this.removed = new TupleSet(); this.watches = new Map(); sources .filter(View.isReactiveSource) @@ -226,7 +226,7 @@ class Watch { || this.source instanceof Formula) { this.callback.call(null, this.source.previousValue, this.source.value) } else { - this.callback.call(null, this.source.set, new JSONSet()); + this.callback.call(null, this.source.set, new TupleSet()); } } detach() { diff --git a/tests.mjs b/tests.mjs index 4d1dd09..479faa0 100644 --- a/tests.mjs +++ b/tests.mjs @@ -1,12 +1,12 @@ -import { _, query, JSONSet } from './datalog.mjs'; +import { _, query, TupleSet } from './datalog.mjs'; const { module, test } = QUnit; -QUnit.assert.queryEquals = function(expect, q, ...args) { -}; +// QUnit.assert.queryEquals = function(expect, q, ...args) { +// }; module('datalog', () => { - let db1 = new JSONSet([ + let db1 = new TupleSet([ ["sally", "age", 21], ["fred", "age", 42], ["ethel", "age", 42], @@ -15,7 +15,7 @@ module('datalog', () => { ["ethel", "likes", "sushi"] ]); - let db2 = new JSONSet([ + let db2 = new TupleSet([ ["ethel", "sex", "female"], ["fred", "sex", "male"] ]); @@ -43,4 +43,13 @@ module('datalog', () => { [["fred"], ["ethel"]] ); }) + test('partial unify', assert => { + assert.deepEqual( + query({ + find: [_.e], + where: [[_.e]] + }, db2).toArray(), + [["ethel"], ["fred"]] + ) + }) }); diff --git a/todo.mjs b/todo.mjs index 8351bf7..8d9f025 100644 --- a/todo.mjs +++ b/todo.mjs @@ -1,60 +1,90 @@ -import { _ } from './datalog.mjs'; +import { query, _ } from './datalog.mjs'; import { transaction, input, formula, database, view, watch } from './reactives.mjs'; -import { $el, on, mapEntities } from './el.mjs'; +import { $el, on, entities } from './el.mjs'; -function nextEid(db) { - return ([...db].map(([eid]) => eid).sort((x, y) => y - x)[0] || 0) + 1; -} +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"] +]); -function todoList() { - 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 textInput = $el.input({type: "text"}); - return $el.div( - $el.form({ - [on.submit]: e => { - let newEid = nextEid(todos); - todos.add([ - [nextEid, "text", textInput.value], - [nextEid, "status", "ready"] - ]) - e.preventDefault(); - } - }, - textInput, - $el.input({type: "submit", value: "Add"}), - ), - mapEntities( - $el.ul(), - todos, - view({find: [_.eid], where: [[_.eid, "text", _._]]})(todos), - ["text", "status"], - ({eid, text, status}) => $el.li( - $el.input({ - type: "button", - value: "x", - [on.click]: todos.remove([...todos].filter(([x]) => x === eid.value)) - }), - $el.span({ - style: formula(s => s === "done" ? "text-decoration: line-through" : "")(status), - [on.click]: () => transaction(() => { - todos.remove([[eid.value, "status", status.value]]); - todos.add([[eid.value, "status", status.value === "done" ? "ready" : "done"]]); - }) - }, text) - ) - ) - ); +function todoList(todos) { + return $el.ul( + entities(todos, attrs => $el.li( + attrs.text + )) + ) } +// function nextEid(db) { +// return ([...db].map(([eid]) => eid).sort((x, y) => y - x)[0] || 0) + 1; +// } + +// function removeEntity(db, eid) { +// db.remove([...db].filter(([x]) => x === eid)) +// } + +// function upsert(db, eid, attrs) { +// transaction(() => { +// // TODO +// }) +// } + +// $el.ul( +// entities(todos, (attrs) => $el.li( +// $el.input({ +// type: "button", +// value: "Delete", +// [on.click]: removeEntity(todos, attr.eid.value) +// }), +// $el.span({ +// style: condp(attr.status, "done", "text-decoration: line-through"), +// [on.click]: upsert(todos, attr.eid.value, {status: statusTransition[attr.status.value]}) +// }, attr.text) +// )) +// ) + +// function todoList(todos) { +// let textInput = $el.input({type: "text"}); +// return $el.div( +// $el.form({ +// [on.submit]: e => { +// let newEid = nextEid(todos); +// todos.add([ +// [nextEid, "text", textInput.value], +// [nextEid, "status", "ready"] +// ]) +// e.preventDefault(); +// } +// }, +// textInput, +// $el.input({type: "submit", value: "Add"}), +// ), +// mapEntities( +// $el.ul(), +// todos, +// view({find: [_.eid], where: [[_.eid, "text", _._]]})(todos), +// ["text", "status"], +// ({eid, text, status}) => $el.li( +// $el.input({ +// type: "button", +// value: "x", +// [on.click]: removeEntity(todos, eid.value) +// }), +// $el.span({ +// style: formula(s => s === "done" ? "text-decoration: line-through" : "")(status), +// [on.click]: upsert(todos, eid.value, {status: status.value === "done" ? "ready" : "done"}) +// }, text) +// ) +// ) +// ); +// } + document.addEventListener("DOMContentLoaded", () => { - document.body.appendChild(todoList()); + document.body.appendChild(todoList(todos)); }); // window.todos = todos;