diff --git a/index.html b/index.html
index 3516d22a7..a6cb2cd6e 100644
--- a/index.html
+++ b/index.html
@@ -24,7 +24,7 @@
diff --git a/src/bootstrap.ts b/src/bootstrap.ts
new file mode 100644
index 000000000..c10ac2197
--- /dev/null
+++ b/src/bootstrap.ts
@@ -0,0 +1,95 @@
+import {Program} from "./runtime/dsl";
+import {Watcher} from "./watchers/watcher";
+import "./watchers/html";
+
+let prog = new Program("test");
+Watcher.attach("html", prog);
+
+prog
+ .block("simple block", ({find, record, lib}) => {
+ let person = find("P");
+ let potato = find("potato");
+ let nameElem;
+ return [
+ nameElem = record("html/element", {tagname: "span", text: person.name}),
+ record("html/element", {tagname: "section", potato}).add("child" + "ren", nameElem)
+ ]
+ });
+
+prog.test(0, [
+ [2, "tag", "html/element"],
+ [2, "tagname", "div"],
+ [2, "children", 3],
+ [2, "sort", 1],
+
+ [3, "tag", "html/element"],
+ [3, "tagname", "span"],
+ [3, "text", "Woo hoo!"],
+ [3, "style", 4],
+
+ [4, "color", "red"],
+ [4, "background", "pink"],
+
+ [5, "tag", "html/element"],
+ [5, "tagname", "div"],
+ [5, "style", 6],
+ [5, "children", 7],
+ [5, "sort", 3],
+
+ [6, "border", "3px solid green"],
+
+ [7, "tag", "html/element"],
+ [7, "tagname", "span"],
+ [7, "text", "meep moop"],
+]);
+
+prog.test(1, [
+ [3, "style", 4, 0, -1]
+]);
+
+prog.test(2, [
+ [3, "style", 4, 0, 1],
+ [4, "font-size", "4em"],
+ [4, "background", "pink", 0, -1]
+]);
+
+prog.test(3, [
+ [8, "tag", "html/element"],
+ [8, "tagname", "div"],
+ [8, "style", 4],
+ [8, "text", "Jeff (from accounting)"],
+ [8, "sort", 0]
+]);
+
+// prog
+// .test(0, [
+// [1, "tag", "P"],
+// [1, "name", "Jeff"],
+
+// [2, "tag", "P"],
+// [2, "name", "KERY"],
+
+// [3, "tag", "P"],
+// [3, "name", "RAB"],
+
+// [4, "tag", "potato"],
+// [4, "kind", "idaho"],
+
+// [5, "tag", "potato"],
+// [5, "kind", "irish gold"],
+
+// ]);
+
+// prog
+// .test(1, [
+// [1, "tag", "P", 0, -1],
+// [2, "tag", "P", 0, -1]
+
+// ]);
+
+// prog
+// .test(2, [
+// [1, "tag", "P", 0, 1],
+// ]);
+
+console.log(prog);
diff --git a/src/runtime/dsl.ts b/src/runtime/dsl.ts
index 63aa34c11..ae1684712 100644
--- a/src/runtime/dsl.ts
+++ b/src/runtime/dsl.ts
@@ -8,7 +8,7 @@ declare var Proxy:new (obj:any, proxy:any) => any;
declare var Symbol:any;
import {RawValue, Register, isRegister, GlobalInterner, Scan, IGNORE_REG, ID,
- InsertNode, Node, Constraint, FunctionConstraint, Change, concatArray} from "./runtime";
+ InsertNode, WatchNode, Node, Constraint, FunctionConstraint, Change, concatArray} from "./runtime";
import * as runtime from "./runtime";
import * as indexes from "./indexes";
@@ -20,6 +20,11 @@ var CURRENT_ID = 0;
// Utils
//--------------------------------------------------------------------
+function toArray(x:T|T[]):T[] {
+ if(x.constructor === Array) return x as T[];
+ return [x as T];
+}
+
function maybeIntern(value:(RawValue|Register)):Register|ID {
if(value === undefined || value === null) throw new Error("Trying to intern an undefined");
if(isRegister(value)) return value;
@@ -210,7 +215,9 @@ class DSLRecord {
__output: boolean = false;
/** If a record is an output, it needs an id by default unless its modifying an existing record. */
__needsId: boolean = true;
- __fields: any;
+
+ __fields: {[field:string]: (RawValue|DSLNode)[]};
+ __dynamicFields: [DSLVariable|string, DSLNode[]][] = [];
constructor(public __block:DSLBlock, tags:string[], initialAttributes:any, entityVariable?:DSLVariable) {
this.__id = CURRENT_ID++;
let fields:any = {tag: tags};
@@ -296,13 +303,18 @@ class DSLRecord {
})
}
- add(attributeName:string, value:DSLNode) {
+ add(attributeName:string|DSLVariable, values:DSLNode|DSLNode[]) {
if(this.__block !== this.__block.program.contextStack[0]) {
throw new Error("Adds and removes may only happen in the root block.");
}
- let record = new DSLRecord(this.__block, [], {[attributeName]: value}, this.__record);
+ values = toArray(values);
+
+ let record = new DSLRecord(this.__block, [], {}, this.__record);
record.__output = true;
this.__block.records.push(record);
+
+ record.__dynamicFields.push([attributeName, values]);
+
return this;
}
@@ -336,14 +348,32 @@ class DSLRecord {
}
toInserts() {
+ let program = this.__block.program;
let inserts:(Constraint|Node)[] = [];
- let e = maybeIntern(toValue(this.__record));
+ let e = maybeIntern(this.__record.value);
+
for(let field in this.__fields) {
for(let dslValue of this.__fields[field]) {
let value = toValue(dslValue) as (RawValue | Register);
- inserts.push(new InsertNode(e, maybeIntern(field), maybeIntern(value), maybeIntern("my-awesome-node")))
+ if(this.__block.watcher) {
+ inserts.push(new WatchNode(e, maybeIntern(field), maybeIntern(value), maybeIntern(program.nodeCount++), this.__block.__id))
+ } else {
+ inserts.push(new InsertNode(e, maybeIntern(field), maybeIntern(value), maybeIntern(program.nodeCount++)))
+ }
+ }
+ for(let [dslField, dslValues] of this.__dynamicFields) {
+ let field = toValue(dslField) as (RawValue | Register);
+ for(let dslValue of dslValues) {
+ let value = toValue(dslValue) as (RawValue | Register);
+ if(this.__block.watcher) {
+ inserts.push(new WatchNode(e, maybeIntern(field), maybeIntern(value), maybeIntern(program.nodeCount++), this.__block.__id))
+ } else {
+ inserts.push(new InsertNode(e, maybeIntern(field), maybeIntern(value), maybeIntern(program.nodeCount++)))
+ }
+ }
}
}
+
return inserts;
}
@@ -382,6 +412,7 @@ class DSLRecord {
//--------------------------------------------------------------------
type DSLCompilable = DSLRecord | DSLFunction;
+export type BlockFunction = (block:DSLBlock) => any;
class DSLBlock {
__id:number;
@@ -401,7 +432,7 @@ class DSLBlock {
lib = this.generateLib();
- constructor(public name:string, public creationFunction:(block:DSLBlock) => any, public readonly program:Program, mangle = true) {
+ constructor(public name:string, public creationFunction:BlockFunction, public readonly program:Program, mangle = true, public readonly watcher = false) {
this.__id = CURRENT_ID++;
let neueFunc = creationFunction;
if(mangle) {
@@ -1096,6 +1127,10 @@ export class Program {
blocks:DSLBlock[] = [];
runtimeBlocks:runtime.Block[] = [];
index:indexes.Index;
+ nodeCount = 0;
+
+ protected _exporter?:runtime.Exporter;
+ protected _lastWatch?:number;
/** Represents the hierarchy of blocks currently being compiled into runtime nodes. */
contextStack:DSLBlock[] = [];
@@ -1104,22 +1139,41 @@ export class Program {
this.index = new indexes.HashIndex();
}
- block(name:string, func:(block:DSLBlock) => any) {
+ block(name:string, func:BlockFunction) {
let block = new DSLBlock(name, func, this);
block.prepare();
this.blocks.push(block);
this.runtimeBlocks.push(block.block);
+
+ return this;
+ }
+
+ watch(name:string, func:BlockFunction) {
+ if(!this._exporter) this._exporter = new runtime.Exporter();
+ let block = new DSLBlock(name, func, this, true, true);
+ block.prepare();
+ this.blocks.push(block);
+ this.runtimeBlocks.push(block.block);
+ this._lastWatch = block.__id;
+ return this;
+ }
+
+ asDiffs(handler:runtime.DiffConsumer) {
+ if(!this._exporter || !this._lastWatch) throw new Error("Must have at least one watch block to export as diffs.");
+ this._exporter.triggerOnDiffs(this._lastWatch, handler);
+
+ return this;
}
input(changes:runtime.Change[]) {
- let trans = new runtime.Transaction(changes[0].transaction, this.runtimeBlocks, changes);
+ let trans = new runtime.Transaction(changes[0].transaction, this.runtimeBlocks, changes, this._exporter && this._exporter.handle);
trans.exec(this.index);
return trans;
}
test(transaction:number, eavns:TestChange[]) {
let changes:Change[] = [];
- let trans = new runtime.Transaction(transaction, this.runtimeBlocks, changes);
+ let trans = new runtime.Transaction(transaction, this.runtimeBlocks, changes, this._exporter && this._exporter.handle);
for(let [e, a, v, round = 0, count = 1] of eavns as EAVRCTuple[]) {
let change = Change.fromValues(e, a, v, "my-awesome-node", transaction, round, count);
if(round === 0) {
@@ -1129,7 +1183,7 @@ export class Program {
}
}
trans.exec(this.index);
- console.log(trans.changes.map((change, ix) => ` <- ${change}`).join("\n"));
+ console.info(trans.changes.map((change, ix) => ` <- ${change}`).join("\n"));
return this;
}
}
diff --git a/src/runtime/runtime.ts b/src/runtime/runtime.ts
index 9346f0255..156bf492f 100644
--- a/src/runtime/runtime.ts
+++ b/src/runtime/runtime.ts
@@ -39,6 +39,12 @@ export function printConstraint(constraint:Constraint) {
}
}
+export function maybeReverse(value?:ID):ID|RawValue|undefined {
+ if(value === undefined) return value;
+ let raw = GlobalInterner.reverse(value);
+ return (""+raw).indexOf("|") === -1 ? raw : value;
+}
+
//------------------------------------------------------------------------
// Runtime
//------------------------------------------------------------------------
@@ -266,7 +272,7 @@ export class Change {
}
toString() {
- return `Change(${GlobalInterner.reverse(this.e)}, ${GlobalInterner.reverse(this.a)}, ${GlobalInterner.reverse(this.v)}, ${GlobalInterner.reverse(this.n)}, ${this.transaction}, ${this.round}, ${this.count})`;
+ return `Change(${this.e}, ${GlobalInterner.reverse(this.a)}, ${maybeReverse(this.v)}, ${GlobalInterner.reverse(this.n)}, ${this.transaction}, ${this.round}, ${this.count})`;
}
equal(other:Change, withoutNode?:boolean, withoutE?:boolean) {
@@ -278,6 +284,24 @@ export class Change {
this.round == other.round &&
this.count == other.count;
}
+
+ reverse(interner:Interner = GlobalInterner) {
+ let {e, a, v, n, transaction, round, count} = this;
+ return new RawChange(interner.reverse(e), interner.reverse(a), interner.reverse(v), interner.reverse(n), transaction, round, count);
+ }
+}
+
+/** A change with all attributes un-interned. */
+export class RawChange {
+ constructor(public e: RawValue, public a: RawValue, public v: RawValue, public n: RawValue,
+ public transaction:number, public round:number, public count:Multiplicity) {}
+
+ toString() {
+ let {e, a, v, n, transaction, round, count} = this;
+ let internedE = GlobalInterner.get(e);
+ let internedV = GlobalInterner.get(v);
+ return `RawChange(${internedE}, ${a}, ${maybeReverse(internedV) || v}, ${n}, ${transaction}, ${round}, ${count})`;
+ }
}
/** A changeset is a list of changes, intended to occur in a single transaction. */
@@ -1022,7 +1046,7 @@ export class JoinNode implements Node {
}
applyCombination(index:Index, input:Change, prefix:ID[], transaction:number, round:number, results:Iterator) {
- debug(" GJ combo:", printPrefix(prefix), prefix);
+ debug(" Join combo:", prefix.slice());
let countOfSolved = 0;
for(let ix = 0; ix < this.registerLookup.length; ix++) {
if(!this.registerLookup[ix]) continue;
@@ -1209,6 +1233,8 @@ export class JoinNode implements Node {
this.unapplyConstraint(constraint, prefix);
}
+ let shouldApply = true;
+
for(let constraintIx = 0; constraintIx < affectedConstraints.length; constraintIx++) {
let mask = 1 << constraintIx;
let isIncluded = (comboIx & mask) !== 0;
@@ -1218,13 +1244,18 @@ export class JoinNode implements Node {
if(isIncluded) {
let valid = constraint.applyInput(input, prefix);
// If any member of the input constraints fails, this whole combination is doomed.
- if(valid === ApplyInputState.fail) break;
+ if(valid === ApplyInputState.fail) {
+ shouldApply = false;
+ break;
+ }
//console.log(" " + printConstraint(constraint));
}
}
//console.log(" ", printPrefix(prefix));
- didSomething = this.applyCombination(index, input, prefix, transaction, round, results) || didSomething;
+ if(shouldApply) {
+ didSomething = this.applyCombination(index, input, prefix, transaction, round, results) || didSomething;
+ }
}
affectedConstraints.reset();
@@ -1257,6 +1288,10 @@ export class InsertNode implements Node {
resolve = Scan.prototype.resolve;
+ key(e:ResolvedValue, a:ResolvedValue, v:ResolvedValue, round:number) {
+ return `${e}|${a}|${v}|${round}`;
+ }
+
exec(index:Index, input:Change, prefix:ID[], transactionId:number, round:number, results:Iterator, transaction:Transaction):boolean {
let {e,a,v,n} = this.resolve(prefix);
@@ -1270,7 +1305,7 @@ export class InsertNode implements Node {
let prefixRound = prefix[prefix.length - 2];
let prefixCount = prefix[prefix.length - 1];
- let key = `${e}|${a}|${v}|${prefixRound + 1}`;
+ let key = this.key(e, a, v, prefixRound + 1);
let prevCount = this.intermediates[key] || 0;
let newCount = prevCount + prefixCount;
this.intermediates[key] = newCount;
@@ -1279,6 +1314,8 @@ export class InsertNode implements Node {
if(prevCount > 0 && newCount <= 0) delta = -1;
if(prevCount <= 0 && newCount > 0) delta = 1;
+ debug(" ?? <-", e, a, v, prefixRound + 1, {prevCount, newCount, delta})
+
if(delta) {
// @TODO: when we do removes, we could say that if the result is a remove, we want to
// dereference these ids instead of referencing them. This would allow us to clean up
@@ -1302,6 +1339,24 @@ export class InsertNode implements Node {
}
}
+export class WatchNode extends InsertNode {
+ constructor(public e:ID|Register,
+ public a:ID|Register,
+ public v:ID|Register,
+ public n:ID|Register,
+ public blockId:number) {
+ super(e, a, v, n);
+ }
+
+ key(e:ResolvedValue, a:ResolvedValue, v:ResolvedValue) {
+ return `${e}|${a}|${v}`;
+ }
+
+ output(transaction:Transaction, change:Change) {
+ transaction.export(this.blockId, change);
+ }
+}
+
//------------------------------------------------------------------------------
// BinaryFlow
//------------------------------------------------------------------------------
@@ -1472,7 +1527,7 @@ export class AntiJoinPresovledRight extends AntiJoin {
}
}
-export class UnionFlow {
+export class UnionFlow implements Node {
constructor(public branches:Node[], public registers:Register[]) { }
exec(index:Index, input:Change, prefix:ID[], transaction:number, round:number, results:Iterator, changes:Transaction):boolean {
@@ -1483,7 +1538,7 @@ export class UnionFlow {
}
}
-export class ChooseFlow {
+export class ChooseFlow implements Node {
branches:Node[] = [];
branchResults:Iterator[] = [];
@@ -1569,10 +1624,10 @@ export class Block {
//------------------------------------------------------------------------------
export class Transaction {
-
round = 0;
protected roundChanges:Change[][] = [];
- constructor(public transaction:number, public blocks:Block[], public changes:Change[]) {}
+ protected exportedChanges:{[blockId:number]: Change[]} = {};
+ constructor(public transaction:number, public blocks:Block[], public changes:Change[], protected exportHandler?:ExportHandler) {}
output(change:Change) {
debug(" <-", change.toString())
@@ -1581,6 +1636,38 @@ export class Transaction {
this.roundChanges[change.round] = cur;
}
+ export(blockId:number, change:Change) {
+ if(!this.exportedChanges[blockId]) this.exportedChanges[blockId] = [change];
+ else this.exportedChanges[blockId].push(change);
+ }
+
+ protected collapseMultiplicity(changes:Change[], results:Change[] /* output */) {
+ // We sort the changes to group all the same EAVs together.
+ // @FIXME: This sort comparator is flawed. It can't differentiate certain EAVs, e.g.:
+ // A: [1, 2, 3]
+ // B: [2, 1, 3]
+ changes.sort((a,b) => (a.e - b.e) + (a.a - b.a) + (a.v - b.v));
+ let changeIx = 0;
+ for(let changeIx = 0; changeIx < changes.length; changeIx++) {
+ let current = changes[changeIx];
+
+ // Collapse each subsequent matching EAV's multiplicity into the current one's.
+ while(changeIx + 1 < changes.length) {
+ let next = changes[changeIx + 1];
+ if(next.e == current.e && next.a == current.a && next.v == current.v) {
+ current.count += next.count;
+ changeIx++;
+ } else {
+ break;
+ }
+ }
+ // console.log("next round change:", current.toString())
+ if(current.count !== 0) results.push(current);
+ }
+
+ return results;
+ }
+
exec(index:Index) {
let {changes, roundChanges} = this;
let changeIx = 0;
@@ -1607,31 +1694,73 @@ export class Transaction {
for(let ix = this.round + 1; ix < maxRound; ix++) {
let nextRoundChanges = roundChanges[ix];
if(nextRoundChanges) {
- nextRoundChanges.sort((a,b) => (a.e - b.e) + (a.a - b.a) + (a.v - b.v))
- let changeIx = 0;
- for(let changeIx = 0; changeIx < nextRoundChanges.length; changeIx++) {
- let current = nextRoundChanges[changeIx];
- while(changeIx + 1 < nextRoundChanges.length) {
- let next = nextRoundChanges[changeIx + 1];
- if(next.e == current.e && next.a == current.a && next.v == current.v) {
- current.count += next.count;
- changeIx++;
- } else {
- break;
- }
- }
- // console.log("next round change:", current.toString())
- if(current.count !== 0) changes.push(current);
- }
- break;
+ let oldLength = changes.length;
+ this.collapseMultiplicity(nextRoundChanges, changes);
+
+ // We only want to break to begin the next fixedpoint when we have something new to run.
+ if(oldLength < changes.length) break;
}
}
}
}
+ let exportingBlocks = Object.keys(this.exportedChanges);
+ if(exportingBlocks.length) {
+ if(!this.exportHandler) throw new Error("Unable to export changes without export handler.");
+
+ for(let blockId of exportingBlocks) {
+ let exports = createArray("exportsArray");
+ this.collapseMultiplicity(this.exportedChanges[+blockId], exports);
+ this.exportedChanges[+blockId] = exports;
+ }
+ this.exportHandler(this.exportedChanges);
+ }
+
// Once the transaction is effectively done, we need to clean up after ourselves. We
// arena allocated a bunch of IDs related to function call outputs, which we can now
// safely release.
GlobalInterner.releaseArena("functionOutput");
}
}
+
+//------------------------------------------------------------------------------
+// Exporter
+//------------------------------------------------------------------------------
+interface Map {[key:number]: V};
+
+type ExportHandler = (blockChanges:Map) => void;
+export type DiffConsumer = (changes:Readonly) => void;
+
+export class Exporter {
+ protected _diffTriggers:Map = {};
+ protected _blocks:ID[] = [];
+
+ triggerOnDiffs(blockId:ID, handler:DiffConsumer):void {
+ if(!this._diffTriggers[blockId]) this._diffTriggers[blockId] = createArray();
+ if(this._diffTriggers[blockId].indexOf(handler) === -1) {
+ this._diffTriggers[blockId].push(handler);
+ }
+ if(this._blocks.indexOf(blockId) === -1) {
+ this._blocks.push(blockId);
+ }
+ }
+
+ handle = (blockChanges:Map) => {
+ for(let blockId of this._blocks) {
+ let changes = blockChanges[blockId];
+ if(changes && changes.length) {
+ let diffTriggers = this._diffTriggers[blockId];
+ if(diffTriggers) {
+ let output:RawChange[] = createArray("exporterOutput");
+ for(let change of changes) {
+ output.push(change.reverse());
+ }
+
+ for(let trigger of diffTriggers) {
+ trigger(output);
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/src/watchers/html.ts b/src/watchers/html.ts
new file mode 100644
index 000000000..575f50b77
--- /dev/null
+++ b/src/watchers/html.ts
@@ -0,0 +1,303 @@
+import {RawValue, RawChange} from "../runtime/runtime";
+import {Watcher} from "./watcher";
+
+interface Map{[key:string]: V}
+
+interface RawRecord extends Map {}
+
+function accumulateChangesAs(changes:RawChange[]) {
+ let adds:Map = {};
+ let removes:Map = {};
+
+ for(let {e, a, v, count} of changes) {
+ if(count === 1) {
+ let record = adds[e] = adds[e] || Object.create(null);
+ if(record[a]) throw new Error("accumulateChanges supports only a single value per attribute.");
+ record[a] = v;
+ } else {
+ let record = removes[e] = removes[e] || Object.create(null);
+ if(record[a]) throw new Error("accumulateChanges supports only a single value per attribute.");
+ record[a] = v;
+ }
+ }
+
+ return {adds, removes};
+}
+
+interface Style extends Map {__size: number}
+interface Instance extends HTMLElement {__element?: string, __styles?: string[], __sort?: number}
+
+class HTMLWatcher extends Watcher {
+ styles:Map