From 80381d175a99fbe33caa1e6c16dbe71924a5df58 Mon Sep 17 00:00:00 2001 From: Joshua Cole Date: Thu, 2 Feb 2017 20:04:56 -0800 Subject: [PATCH 01/22] Add transaction export, Exporter, Program.watch, and Watcher registry --- src/runtime/dsl.ts | 40 ++++++++-- src/runtime/runtime.ts | 161 +++++++++++++++++++++++++++++++++++----- src/watchers/html.ts | 35 +++++++++ src/watchers/watcher.ts | 32 ++++++++ 4 files changed, 242 insertions(+), 26 deletions(-) create mode 100644 src/watchers/html.ts create mode 100644 src/watchers/watcher.ts diff --git a/src/runtime/dsl.ts b/src/runtime/dsl.ts index e0b2cac60..ad3a353ab 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"; @@ -298,6 +298,7 @@ class DSLRecord { } toInserts() { + let program = this.__block.program; let inserts:(Constraint|Node)[] = []; let e = maybeIntern(toValue(this.__record)); let values = []; @@ -306,7 +307,11 @@ class DSLRecord { let value = toValue(dslValue) as (RawValue | Register); // @TODO: generate node ids values.push(maybeIntern(value)); - 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++))) + } else { + inserts.push(new InsertNode(e, maybeIntern(field), maybeIntern(value), maybeIntern(program.nodeCount++))) + } } } if(this.__needsId) { @@ -333,6 +338,7 @@ class DSLRecord { //-------------------------------------------------------------------- type DSLCompilable = DSLRecord | DSLFunction; +export type BlockFunction = (block:DSLBlock) => any; class DSLBlock { records:DSLRecord[] = []; @@ -350,7 +356,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) { let neueFunc = creationFunction; if(mangle) { let functionArgs:string[] = []; @@ -861,6 +867,9 @@ export class Program { blocks:DSLBlock[] = []; runtimeBlocks:runtime.Block[] = []; index:indexes.Index; + nodeCount = 0; + + protected _exporter?:runtime.Exporter; /** Represents the hierarchy of blocks currently being compiled into runtime nodes. */ contextStack:DSLBlock[] = []; @@ -869,22 +878,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); + + return this; + } + + asDiffs(tag:string, handler:runtime.DiffConsumer) { + if(!this._exporter) throw new Error("Must have at least one watch block to export as diffs."); + this._exporter.triggerOnDiffs(GlobalInterner.intern(tag), 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) { diff --git a/src/runtime/runtime.ts b/src/runtime/runtime.ts index 0595c0102..5becf8f78 100644 --- a/src/runtime/runtime.ts +++ b/src/runtime/runtime.ts @@ -278,6 +278,22 @@ 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; + return `RawChange(${e}, ${a}, ${v}, ${n}, ${transaction}, ${round}, ${count})`; + } } /** A changeset is a list of changes, intended to occur in a single transaction. */ @@ -1236,6 +1252,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); @@ -1249,7 +1269,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; @@ -1281,6 +1301,16 @@ export class InsertNode implements Node { } } +export class WatchNode extends InsertNode { + key(e:ResolvedValue, a:ResolvedValue, v:ResolvedValue) { + return `${e}|${a}|${v}`; + } + + output(transaction:Transaction, change:Change) { + transaction.export(change); + } +} + //------------------------------------------------------------------------------ // BinaryFlow //------------------------------------------------------------------------------ @@ -1543,10 +1573,10 @@ export class Block { //------------------------------------------------------------------------------ export class Transaction { - round = 0; protected roundChanges:Change[][] = []; - constructor(public transaction:number, public blocks:Block[], public changes:Change[]) {} + protected exportedChanges:Change[] = []; + constructor(public transaction:number, public blocks:Block[], public changes:Change[], protected exportHandler?:ExportHandler) {} output(change:Change) { debug(" <-", change.toString()) @@ -1555,6 +1585,37 @@ export class Transaction { this.roundChanges[change.round] = cur; } + export(change:Change) { + this.exportedChanges.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; @@ -1581,31 +1642,91 @@ 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; } } } } + if(this.exportedChanges.length) { + console.log("Pre:"); + console.log(" " + this.exportedChanges.join("\n ")); + + + if(!this.exportHandler) throw new Error("Unable to export changes without export handler."); + let exports = createArray("exportsArray"); + this.collapseMultiplicity(this.exportedChanges, exports); + if(exports.length) { + console.log("Exporting:"); + console.log(" " + exports.join("\n ")); + this.exportHandler(exports); + } + } + // 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 TagMap {[tag:number]: V}; +interface EntityMap {[entityId:number]: V}; +interface AttributeMap {[attributeId:number]: V}; +interface Record {[attribute:string]: RawValue[]}; + +type ExportHandler = (changes:Change[]) => void; +export type DiffConsumer = (changes:Readonly) => void; + +const TAG = GlobalInterner.intern("tag"); + +export class Exporter { + protected _diffTriggers:TagMap = {}; + + triggerOnDiffs(tag:ID, handler:DiffConsumer):void { + if(!this._diffTriggers[tag]) this._diffTriggers[tag] = createArray(); + if(this._diffTriggers[tag].indexOf(handler) === -1) { + this._diffTriggers[tag].push(handler); + } + } + + handle = (changes:Change[]) => { + let newByTags:TagMap = {}; + let entityChanges:EntityMap = {}; + for(let change of changes) { + if(change.a === TAG) { + if(!newByTags[change.v]) newByTags[change.v] = createArray("exporterNewTags"); + newByTags[change.v].push(change.e); + } + + if(!entityChanges[change.e]) entityChanges[change.e] = createArray("exporterEntityChanges"); + entityChanges[change.e].push(change); + } + + // @NOTE: We're leaving a lot of perf on the table right now. It's not a priority atm. + for(let rawTag of Object.keys(newByTags)) { + let tag:ID = +rawTag; + let entityIds = newByTags[tag]; + if(this._diffTriggers[tag]) { + let output:RawChange[] = createArray("exporterOutput"); + for(let entityId of newByTags[tag]) { + for(let change of entityChanges[entityId]) { + // We'll omit the tag you're listening to so it doesn't pollute your diffs. + if(change.a === TAG && change.v === tag) continue; + output.push(change.reverse()); + } + } + for(let trigger of this._diffTriggers[tag]) { + trigger(output); + } + } + } + } +} diff --git a/src/watchers/html.ts b/src/watchers/html.ts new file mode 100644 index 000000000..746e24338 --- /dev/null +++ b/src/watchers/html.ts @@ -0,0 +1,35 @@ +import {Watcher} from "./watcher"; + +class HTMLWatcher extends Watcher { + setup() { + this.program + .watch("Elements with no parents are roots.", ({find, record, lib, not}) => { + let elem = find("html/element"); + not(({find}) => { + find("html/element", {children: elem}); + }); + return [ + record("html/root", {element: elem, tagname: elem.tagname}) + ]; + }) + .asDiffs("html/root", (changes) => { + console.log("Diffs: (html/root)"); + console.log(" " + changes.join("\n ")); + }) + .watch("Create an instance for each child of a parent.", ({find, record, lib, not}) => { + let elem = find("html/element"); + let parent = find("html/element", {children: elem}); + + return [ + record("html/instance", {element: elem, tagname: elem.tagname, parent}) + ]; + }) + .asDiffs("html/instance", (changes) => { + console.log("Diffs: (html/instance)"); + console.log(" " + changes.join("\n ")); + }); + + } +} + +Watcher.register("html", HTMLWatcher); diff --git a/src/watchers/watcher.ts b/src/watchers/watcher.ts new file mode 100644 index 000000000..c056ed830 --- /dev/null +++ b/src/watchers/watcher.ts @@ -0,0 +1,32 @@ +import {Program, BlockFunction} from "../runtime/dsl"; + +export class Watcher { + protected static _registry:{[id:string]: typeof Watcher} = {}; + + static register(id:string, watcher:typeof Watcher) { + if(this._registry[id]) { + if(this._registry[id] === watcher) return; + throw new Error(`Attempting to overwrite existing watcher with id '${id}'`); + } + this._registry[id] = watcher; + } + + static unregister(id:string) { + delete this._registry[id]; + } + + static attach(id:string, program:Program) { + if(!this._registry[id]) throw new Error("Unable to attach unknown watcher."); + let watcher = new this._registry[id](program); + return watcher; + } + + + get program() { return this._program; } + + constructor(protected _program:Program) { + this.setup(); + } + + setup() {} +} From 357fb3ef4917379a8201d8c2905bb15e77759282 Mon Sep 17 00:00:00 2001 From: Joshua Cole Date: Thu, 2 Feb 2017 20:05:10 -0800 Subject: [PATCH 02/22] Mode the index file to point at bootstrap.ts --- index.html | 2 +- src/bootstrap.ts | 25 +++++++++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) create mode 100644 src/bootstrap.ts 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..e16780cf0 --- /dev/null +++ b/src/bootstrap.ts @@ -0,0 +1,25 @@ +import {Program} from "./runtime/dsl"; +import {Watcher} from "./watchers/watcher"; +import "./watchers/html"; + +let prog = new Program("test"); +Watcher.attach("html", prog); + +// prog.watch("simple block", ({find, record, lib}) => { +// find({foo: "bar"}); +// return [ +// record("watch-result", {zomg: "baz"}) +// ] +// }).asDiffs("watch-result", (changes) => { +// console.log(changes); +// }); + +prog.test(1, [ + [2, "tag", "html/element"], + [2, "tagname", "div"], + [2, "children", 3], + + [3, "tag", "html/element"], + [3, "tagname", "floop"], + [3, "text", "k"], +]); From 2a9f18abe0e2e3da19b368d1802aeb4ecfd9cdfa Mon Sep 17 00:00:00 2001 From: Joshua Cole Date: Fri, 3 Feb 2017 13:09:20 -0800 Subject: [PATCH 03/22] Less logging --- src/runtime/runtime.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/runtime/runtime.ts b/src/runtime/runtime.ts index 5becf8f78..ba4a027d4 100644 --- a/src/runtime/runtime.ts +++ b/src/runtime/runtime.ts @@ -1653,16 +1653,12 @@ export class Transaction { } if(this.exportedChanges.length) { - console.log("Pre:"); - console.log(" " + this.exportedChanges.join("\n ")); - - if(!this.exportHandler) throw new Error("Unable to export changes without export handler."); let exports = createArray("exportsArray"); this.collapseMultiplicity(this.exportedChanges, exports); if(exports.length) { - console.log("Exporting:"); - console.log(" " + exports.join("\n ")); + // console.log("Exporting:"); + // console.log(" " + exports.join("\n ")); this.exportHandler(exports); } } From 6134ba0ee00de45f9db6ab0aabeeca4a6c897c05 Mon Sep 17 00:00:00 2001 From: Joshua Cole Date: Mon, 6 Feb 2017 11:09:28 -0800 Subject: [PATCH 04/22] Get basic rendering working --- src/watchers/html.ts | 228 ++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 217 insertions(+), 11 deletions(-) diff --git a/src/watchers/html.ts b/src/watchers/html.ts index 746e24338..d3171676f 100644 --- a/src/watchers/html.ts +++ b/src/watchers/html.ts @@ -1,34 +1,240 @@ +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} + class HTMLWatcher extends Watcher { + styles:Map = Object.create(null); + roots:Map = Object.create(null); + instances:Map = Object.create(null); + styleToInstances:Map = Object.create(null); + + getStyle(id:string) { + return this.styles[id] = this.styles[id] || {__size: 0}; + } + + getInstance(id:string, tagname:RawValue = "div"):Instance { + if(this.roots[id]) return this.roots[id]!; + return this.instances[id] = this.instances[id] || document.createElement(tagname as string); + } + + clearInstance(id:string) { + this.instances[id] = undefined; + } + + getRoot(id:string, tagname:RawValue = "div"):Instance { + return this.roots[id] = this.roots[id] || document.createElement(tagname as string); + } + + clearRoot(id:string) { + this.roots[id] = undefined; + } + + insertChild(parent:Instance, child:Instance) { + parent.appendChild(child); + } + + setStyleAttribute(style:Style, attribute:string, value:RawValue, count:-1|1) { + if(count === -1) { + if(!style[attribute]) throw new Error(`Cannot remove non-existent attribute '${attribute}'`); + if(style[attribute] !== value) throw new Error(`Cannot remove mismatched AV ${attribute}: ${value} (current: ${style[attribute]})`); + style[attribute] = undefined; + } else { + if(style[attribute]) throw new Error(`Cannot add already present attribute '${attribute}'`); + style[attribute] = value; + } + style.__size += count; + } + setup() { this.program - .watch("Elements with no parents are roots.", ({find, record, lib, not}) => { + .block("Elements with no parents are roots.", ({find, record, lib, not}) => { let elem = find("html/element"); not(({find}) => { find("html/element", {children: elem}); }); return [ - record("html/root", {element: elem, tagname: elem.tagname}) + record("html/root", "html/instance", {element: elem, tagname: elem.tagname}) ]; }) - .asDiffs("html/root", (changes) => { - console.log("Diffs: (html/root)"); - console.log(" " + changes.join("\n ")); - }) - .watch("Create an instance for each child of a parent.", ({find, record, lib, not}) => { + .block("Create an instance for each child of a rooted parent.", ({find, record, lib, not}) => { let elem = find("html/element"); - let parent = find("html/element", {children: elem}); + let parentElem = find("html/element", {children: elem}); + let parent = find("html/instance", {element: parentElem}); return [ record("html/instance", {element: elem, tagname: elem.tagname, parent}) ]; }) + .watch("Export all instances.", ({find, record}) => { + let instance = find("html/instance"); + return [ + record("html/instance", {tagname: instance.tagname, element: instance.element, instance}) + ]; + }) .asDiffs("html/instance", (changes) => { - console.log("Diffs: (html/instance)"); - console.log(" " + changes.join("\n ")); - }); + // console.log("Diffs: (html/instance)"); + // console.log(" " + changes.join("\n ")); + + let diff = accumulateChangesAs<{tagname:string, element:string, instance:string}>(changes); + for(let e of Object.keys(diff.removes)) { + let {instance:instanceId} = diff.removes[e]; + this.clearInstance(instanceId); + } + for(let e of Object.keys(diff.adds)) { + let {instance:instanceId, tagname, element} = diff.adds[e]; + let instance = this.getInstance(instanceId, tagname); + instance.__element = element; + } + }) + .watch("Export roots.", ({find, record}) => { + let root = find("html/root"); + return [ + record("html/root", {instance: root}) + ]; + }) + .asDiffs("html/root", (changes) => { + for(let {e, a, v:rootId, count} of changes) { + if(count === 1) { + let root = this.roots[rootId] = this.getInstance(rootId); + document.body.appendChild(root); + } else { + let root = this.roots[rootId]; + if(root) { + document.body.removeChild(root); + } + } + } + }) + + .watch("Export instance parents.", ({find, record}) => { + let instance = find("html/instance"); + return [ + record("html/parent", {instance, parent: instance.parent}) + ]; + }) + .asDiffs("html/parent", (changes) => { + // console.log("Diffs: (html/parent)"); + // console.log(" " + changes.join("\n ")); + + let diff = accumulateChangesAs<{instance:string, parent:string}>(changes); + for(let e of Object.keys(diff.removes)) { + let {instance:instanceId, parent:parentId} = diff.removes[e]; + if(this.instances[parentId]) { + this.getInstance(parentId).removeChild(this.getInstance(instanceId)); + } + } + for(let e of Object.keys(diff.adds)) { + let {instance:instanceId, parent:parentId} = diff.adds[e]; + let instance = this.getInstance(instanceId); + this.insertChild(this.getInstance(parentId), instance); + } + }) + + .block("Records in the style attribute of an element are styles.", ({find, record, lib, not}) => { + let elem = find("html/element"); + let style = elem.style; + return [ + style.add("tag", "html/style") + ]; + }) + .watch("Export html styles.", ({find, record, lib, not, lookup}) => { + let style = find("html/style"); + let {attribute, value} = lookup(style); + return [ + style.add("tag", "html/style") + .add(attribute, value) + ]; + }) + .asDiffs("html/style", (changes) => { + // console.log("Diffs: (html/style)"); + // console.log(" " + changes.join("\n ")); + + let changed = []; + for(let {e:styleId, a, v, count} of changes) { + changed.push(styleId); + let style = this.getStyle(styleId); + this.setStyleAttribute(style, a, v, count); + + let instances = this.styleToInstances[styleId]; + if(instances) { + for(let instanceId of instances) { + let instance = this.getInstance(instanceId); + instance.style[a] = style[a] as any; + } + } + } + + for(let styleId of changed) { + let style = this.getStyle(styleId); + if(style.__size === 0) { + this.styles[styleId] = undefined; + } + } + }) + + .watch("Export element attributes.", ({find, record, lookup}) => { + let instance = find("html/instance"); + let elem = instance.element; + let {attribute, value} = lookup(elem); + return [ + instance + .add("tag", "html/attribute") + .add(attribute, value) + ]; + }) + .asDiffs("html/attribute", (changes) => { + // console.log("Diffs: (html/attribute)"); + // console.log(" " + changes.join("\n ")); + + for(let {e, a, v, count} of changes) { + let instance = this.getInstance(e); + if(a === "text") { + instance.textContent = count > 0 ? v : undefined; + + } else if(a === "style") { + this.styleToInstances[v] = e; + let style = this.getStyle(v); + for(let prop of Object.keys(style)) { + if(prop === "__size") continue; + instance.style[prop as any] = style[prop] as string; + } + + } else { + if(count === 1) { + instance.setAttribute(a, v); + } else { + instance.removeAttribute(a); + } + } + } + }); + // console.log(this); } } From 8df120e3395af9d5d32f7c612667b8d05a9c543e Mon Sep 17 00:00:00 2001 From: Joshua Cole Date: Mon, 6 Feb 2017 15:01:03 -0800 Subject: [PATCH 05/22] Fix impossible removal bug --- src/watchers/html.ts | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/watchers/html.ts b/src/watchers/html.ts index d3171676f..661c66705 100644 --- a/src/watchers/html.ts +++ b/src/watchers/html.ts @@ -43,6 +43,10 @@ class HTMLWatcher extends Watcher { } clearInstance(id:string) { + let instance = this.instances[id]; + if(instance && instance.parentElement) { + instance.parentElement.removeChild(instance); + } this.instances[id] = undefined; } @@ -139,14 +143,17 @@ class HTMLWatcher extends Watcher { ]; }) .asDiffs("html/parent", (changes) => { - // console.log("Diffs: (html/parent)"); - // console.log(" " + changes.join("\n ")); + console.log("Diffs: (html/parent)"); + console.log(" " + changes.join("\n ")); let diff = accumulateChangesAs<{instance:string, parent:string}>(changes); for(let e of Object.keys(diff.removes)) { let {instance:instanceId, parent:parentId} = diff.removes[e]; if(this.instances[parentId]) { - this.getInstance(parentId).removeChild(this.getInstance(instanceId)); + let instance = this.instances[instanceId]; + if(instance && instance.parentElement) { + instance.parentElement.removeChild(instance); + } } } for(let e of Object.keys(diff.adds)) { From 9acd6f9cda6726b3805289f8c6eba44b17babece Mon Sep 17 00:00:00 2001 From: Joshua Cole Date: Mon, 6 Feb 2017 15:02:16 -0800 Subject: [PATCH 06/22] Update example to test crossy parent --- src/bootstrap.ts | 88 ++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 70 insertions(+), 18 deletions(-) diff --git a/src/bootstrap.ts b/src/bootstrap.ts index e16780cf0..5a35fc811 100644 --- a/src/bootstrap.ts +++ b/src/bootstrap.ts @@ -5,21 +5,73 @@ import "./watchers/html"; let prog = new Program("test"); Watcher.attach("html", prog); -// prog.watch("simple block", ({find, record, lib}) => { -// find({foo: "bar"}); -// return [ -// record("watch-result", {zomg: "baz"}) -// ] -// }).asDiffs("watch-result", (changes) => { -// console.log(changes); -// }); - -prog.test(1, [ - [2, "tag", "html/element"], - [2, "tagname", "div"], - [2, "children", 3], - - [3, "tag", "html/element"], - [3, "tagname", "floop"], - [3, "text", "k"], -]); +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], + +// [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], + +// [6, "border", "3px solid green"], + +// [7, "tag", "html/element"], +// [7, "tagname", "span"], +// [7, "text", "meep moop"] +// ]); + +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"], + + ]); + +console.log("#############################################################"); +prog + .test(1, [ + [1, "tag", "P", 0, -1], + //[3, "tag", "P", 0, -1] + ]); + +// console.log(prog); + +console.log("$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$"); + +prog + .test(2, [ + [3, "tag", "P", 0, 1], + ]); From 3fbf3bd53855c73a02c2d7ea49cd5288ca158798 Mon Sep 17 00:00:00 2001 From: Joshua Cole Date: Mon, 6 Feb 2017 15:25:15 -0800 Subject: [PATCH 07/22] Sideport the dynamic add changes --- src/runtime/dsl.ts | 38 ++++++++++++++++++++++++++++++++++---- 1 file changed, 34 insertions(+), 4 deletions(-) diff --git a/src/runtime/dsl.ts b/src/runtime/dsl.ts index ad3a353ab..c7eeca20f 100644 --- a/src/runtime/dsl.ts +++ b/src/runtime/dsl.ts @@ -19,6 +19,11 @@ const UNASSIGNED = -1; // 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; @@ -187,7 +192,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, DSLNode[]][] = []; constructor(public __block:DSLBlock, tags:string[], initialAttributes:any, entityVariable?:DSLVariable) { let fields:any = {tag: tags}; for(let field in initialAttributes) { @@ -272,13 +279,23 @@ 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); + + if(typeof attributeName === "string") { + record.__fields[attributeName] = values; + + } else { + record.__dynamicFields.push([attributeName, values]); + } + return this; } @@ -300,7 +317,8 @@ class DSLRecord { toInserts() { let program = this.__block.program; let inserts:(Constraint|Node)[] = []; - let e = maybeIntern(toValue(this.__record)); + let e = maybeIntern(this.__record.value); + let values = []; for(let field in this.__fields) { for(let dslValue of this.__fields[field]) { @@ -313,10 +331,22 @@ class DSLRecord { inserts.push(new InsertNode(e, maybeIntern(field), maybeIntern(value), maybeIntern(program.nodeCount++))) } } + for(let [dslField, dslValues] of this.__dynamicFields) { + let field = toValue(dslField) as Register; + for(let dslValue of dslValues) { + let value = toValue(dslValue) as (RawValue | Register); + if(this.__block.watcher) { + inserts.push(new WatchNode(e, field, maybeIntern(value), maybeIntern(program.nodeCount++))) + } else { + inserts.push(new InsertNode(e, field, maybeIntern(value), maybeIntern(program.nodeCount++))) + } + } + } } if(this.__needsId) { inserts.push(FunctionConstraint.create("eve/internal/gen-id", {result: e}, values) as FunctionConstraint); } + return inserts; } From f1a0107659fff4210bd652be55b50033da24addf Mon Sep 17 00:00:00 2001 From: Joshua Cole Date: Mon, 6 Feb 2017 15:58:21 -0800 Subject: [PATCH 08/22] Fix root removal + ignore used attrs --- src/watchers/html.ts | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/src/watchers/html.ts b/src/watchers/html.ts index 661c66705..c3dfe9739 100644 --- a/src/watchers/html.ts +++ b/src/watchers/html.ts @@ -123,14 +123,17 @@ class HTMLWatcher extends Watcher { ]; }) .asDiffs("html/root", (changes) => { + // console.log("Diffs: (html/root)"); + // console.log(" " + changes.join("\n ")); + for(let {e, a, v:rootId, count} of changes) { if(count === 1) { let root = this.roots[rootId] = this.getInstance(rootId); document.body.appendChild(root); } else { let root = this.roots[rootId]; - if(root) { - document.body.removeChild(root); + if(root && root.parentElement) { + root.parentElement.removeChild(root); } } } @@ -143,8 +146,8 @@ class HTMLWatcher extends Watcher { ]; }) .asDiffs("html/parent", (changes) => { - console.log("Diffs: (html/parent)"); - console.log(" " + changes.join("\n ")); + // console.log("Diffs: (html/parent)"); + // console.log(" " + changes.join("\n ")); let diff = accumulateChangesAs<{instance:string, parent:string}>(changes); for(let e of Object.keys(diff.removes)) { @@ -232,6 +235,15 @@ class HTMLWatcher extends Watcher { instance.style[prop as any] = style[prop] as string; } + } else if(a === "tagname") { + if((""+v).toUpperCase() !== instance.tagName) { + // handled by html/instance + html/root + throw new Error("Unable to change element tagname."); + } + + } else if(a === "children") { + // Handled by html/parent + } else { if(count === 1) { instance.setAttribute(a, v); From 4c11db71a363ca06d12fcaf9cf9d3a133827fb0e Mon Sep 17 00:00:00 2001 From: Joshua Cole Date: Mon, 6 Feb 2017 15:58:53 -0800 Subject: [PATCH 09/22] Fix diff ordering in exporter --- src/runtime/runtime.ts | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/src/runtime/runtime.ts b/src/runtime/runtime.ts index ba4a027d4..e9bb449d6 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) return undefined; + 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) { @@ -292,7 +298,9 @@ export class RawChange { toString() { let {e, a, v, n, transaction, round, count} = this; - return `RawChange(${e}, ${a}, ${v}, ${n}, ${transaction}, ${round}, ${count})`; + let internedE = GlobalInterner.get(e); + let internedV = GlobalInterner.get(v); + return `RawChange(${internedE}, ${a}, ${maybeReverse(internedV) || v}, ${n}, ${transaction}, ${round}, ${count})`; } } @@ -1685,12 +1693,16 @@ const TAG = GlobalInterner.intern("tag"); export class Exporter { protected _diffTriggers:TagMap = {}; + protected _tags:ID[] = []; triggerOnDiffs(tag:ID, handler:DiffConsumer):void { if(!this._diffTriggers[tag]) this._diffTriggers[tag] = createArray(); if(this._diffTriggers[tag].indexOf(handler) === -1) { this._diffTriggers[tag].push(handler); } + if(this._tags.indexOf(tag) === -1) { + this._tags.push(tag); + } } handle = (changes:Change[]) => { @@ -1707,8 +1719,9 @@ export class Exporter { } // @NOTE: We're leaving a lot of perf on the table right now. It's not a priority atm. - for(let rawTag of Object.keys(newByTags)) { - let tag:ID = +rawTag; + for(let tag of this._tags) { + if(!newByTags[tag]) continue; + let entityIds = newByTags[tag]; if(this._diffTriggers[tag]) { let output:RawChange[] = createArray("exporterOutput"); From 79986406518ddcdd34af145888d62d744328b3cc Mon Sep 17 00:00:00 2001 From: Joshua Cole Date: Mon, 6 Feb 2017 16:40:00 -0800 Subject: [PATCH 10/22] Remove logging --- src/runtime/dsl.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/runtime/dsl.ts b/src/runtime/dsl.ts index c7eeca20f..77669e16f 100644 --- a/src/runtime/dsl.ts +++ b/src/runtime/dsl.ts @@ -952,7 +952,7 @@ export class Program { } } trans.exec(this.index); - console.log(trans.changes.map((change, ix) => ` <- ${change}`).join("\n")); + // console.log(trans.changes.map((change, ix) => ` <- ${change}`).join("\n")); return this; } } From 90ecb7a1abde5ade3337c10d04becdb4ab0e76d4 Mon Sep 17 00:00:00 2001 From: Joshua Cole Date: Mon, 6 Feb 2017 16:40:26 -0800 Subject: [PATCH 11/22] Simpler example of the jeff issue --- src/bootstrap.ts | 23 +++++++++-------------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/src/bootstrap.ts b/src/bootstrap.ts index 5a35fc811..4a948d0d0 100644 --- a/src/bootstrap.ts +++ b/src/bootstrap.ts @@ -46,32 +46,27 @@ prog [1, "tag", "P"], [1, "name", "Jeff"], - [2, "tag", "P"], - [2, "name", "KERY"], + // [2, "tag", "P"], + // [2, "name", "KERY"], // [3, "tag", "P"], - [3, "name", "RAB"], + // [3, "name", "RAB"], [4, "tag", "potato"], - [4, "kind", "idaho"], + // [4, "kind", "idaho"], - [5, "tag", "potato"], - [5, "kind", "irish gold"], + // [5, "tag", "potato"], + // [5, "kind", "irish gold"], ]); -console.log("#############################################################"); prog .test(1, [ - [1, "tag", "P", 0, -1], - //[3, "tag", "P", 0, -1] + [1, "tag", "P", 0, -1] ]); -// console.log(prog); - -console.log("$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$"); - prog .test(2, [ - [3, "tag", "P", 0, 1], + //[3, "tag", "P", 0, 1], + [1, "tag", "P", 0, 1], ]); From 8f5195b2033e9c43a10a36edb31e4d699ef37ffe Mon Sep 17 00:00:00 2001 From: Chris Granger Date: Mon, 6 Feb 2017 18:07:56 -0800 Subject: [PATCH 12/22] Fix up type implementations for choose and union --- src/bootstrap.ts | 11 ++++---- src/runtime/runtime.ts | 58 ++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 62 insertions(+), 7 deletions(-) diff --git a/src/bootstrap.ts b/src/bootstrap.ts index 4a948d0d0..8ea8a1f17 100644 --- a/src/bootstrap.ts +++ b/src/bootstrap.ts @@ -65,8 +65,9 @@ prog [1, "tag", "P", 0, -1] ]); -prog - .test(2, [ - //[3, "tag", "P", 0, 1], - [1, "tag", "P", 0, 1], - ]); +//prog +// .test(2, [ +// //[3, "tag", "P", 0, 1], +// [1, "tag", "P", 0, 1], +// ]); +console.log(prog); diff --git a/src/runtime/runtime.ts b/src/runtime/runtime.ts index e9bb449d6..9a17c1041 100644 --- a/src/runtime/runtime.ts +++ b/src/runtime/runtime.ts @@ -951,6 +951,9 @@ type ResolvedEAVN = {e:ResolvedValue, a:ResolvedValue, v:ResolvedValue, n:Resolv * Base class for nodes, the building blocks of blocks. */ export interface Node { + /** Reset any state associated with this node in-between rounds */ + reset():void; + /** * Evaluate the node in the context of the currently solved prefix, * returning a set of valid prefixes to continue the query as @@ -1019,6 +1022,8 @@ export class JoinNode implements Node { this.proposedResultsArrays = proposedResultsArrays; } + reset() {} + findAffectedConstraints(input:Change, prefix:ID[]):Iterator { // @TODO: Hoist me out. let affectedConstraints = this.affectedConstraints; @@ -1036,6 +1041,7 @@ export class JoinNode implements Node { } applyCombination(index:Index, input:Change, prefix:ID[], transaction:number, round:number, results:Iterator) { + debug(" Join combo:", prefix.toString()); let countOfSolved = 0; for(let field of prefix) { if(field !== undefined) countOfSolved++; @@ -1250,6 +1256,7 @@ export class JoinNode implements Node { export class InsertNode implements Node { intermediates:{[key:string]: number|undefined} = {}; + deduper:{[key:number]: ID[][]} = {}; constructor(public e:ID|Register, public a:ID|Register, @@ -1260,11 +1267,51 @@ export class InsertNode implements Node { resolve = Scan.prototype.resolve; + reset() { + this.deduper = {}; + } + + prefixKey(prefix:ID[]) { + let key = 0; + for(let ix = 0, len = prefix.length; ix < len; ix++) { + let value = +prefix[ix]; + key += value; + } + if(isNaN(key)) throw new Error("Got prefix in InsertNode that has an undefined in it"); + return key; + } + + dedupe(prefix:ID[]) { + let {deduper} = this; + let prefixKey = this.prefixKey(prefix); + let matched = false; + let found = deduper[prefixKey]; + if(found) { + maybePrefixLoop: for(let maybePrefix of found) { + if(maybePrefix.length !== prefix.length) continue; + for(let ix = 0, len = prefix.length; ix < len; ix++) { + if(maybePrefix[ix] != prefix[ix]) continue maybePrefixLoop; + } + matched = true; + break; + } + } + if(!matched) { + if(!found) deduper[prefixKey] = [prefix]; + else found.push(prefix); + return true; + } + return false; + } + 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 { + if(!this.dedupe(prefix)) { + return true; + } let {e,a,v,n} = this.resolve(prefix); // @FIXME: This is pretty wasteful to copy one by one here. @@ -1357,6 +1404,8 @@ abstract class BinaryFlow implements Node { constructor(public left:Node, public right:Node) { } + reset() {} + exec(index:Index, input:Change, prefix:ID[], transaction:number, round:number, results:Iterator, changes:Transaction):boolean { let {left, right, leftResults, rightResults} = this; leftResults.clear(); @@ -1486,9 +1535,11 @@ export class AntiJoinPresovledRight extends AntiJoin { } } -export class UnionFlow { +export class UnionFlow implements Node { constructor(public branches:Node[], public registers:Register[]) { } + reset() {} + exec(index:Index, input:Change, prefix:ID[], transaction:number, round:number, results:Iterator, changes:Transaction):boolean { for(let node of this.branches) { node.exec(index, input, prefix, transaction, round, results, changes); @@ -1497,7 +1548,7 @@ export class UnionFlow { } } -export class ChooseFlow { +export class ChooseFlow implements Node { branches:Node[] = []; branchResults:Iterator[] = []; @@ -1515,6 +1566,8 @@ export class ChooseFlow { } } + reset() {} + exec(index:Index, input:Change, prefix:ID[], transaction:number, round:number, results:Iterator, changes:Transaction):boolean { let {branchResults, branches} = this; let prev:Iterator|undefined; @@ -1559,6 +1612,7 @@ export class Block { // We populate the prefix with values from the input change so we only derive the // results affected by it. for(let node of this.nodes) { + node.reset(); while((prefix = this.results.next()) !== undefined) { let valid = node.exec(index, input, prefix, transaction.transaction, transaction.round, this.nextResults, transaction); if(!valid) { From 243a2343c745451f2b6211ff1a4c4c132f94c52e Mon Sep 17 00:00:00 2001 From: Joshua Cole Date: Mon, 6 Feb 2017 18:23:21 -0800 Subject: [PATCH 13/22] Dont ignore count for special attributes in html watcher --- src/watchers/html.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/watchers/html.ts b/src/watchers/html.ts index c3dfe9739..da50295de 100644 --- a/src/watchers/html.ts +++ b/src/watchers/html.ts @@ -223,7 +223,8 @@ class HTMLWatcher extends Watcher { // console.log(" " + changes.join("\n ")); for(let {e, a, v, count} of changes) { - let instance = this.getInstance(e); + let instance = this.instances[e]; + if(!instance) continue; if(a === "text") { instance.textContent = count > 0 ? v : undefined; @@ -232,10 +233,15 @@ class HTMLWatcher extends Watcher { let style = this.getStyle(v); for(let prop of Object.keys(style)) { if(prop === "__size") continue; - instance.style[prop as any] = style[prop] as string; + if(count > 0) { + instance.style[prop as any] = style[prop] as string; + } else { + throw new Error("@TODO: Implement me!"); + } } } else if(a === "tagname") { + if(count < 0) continue; if((""+v).toUpperCase() !== instance.tagName) { // handled by html/instance + html/root throw new Error("Unable to change element tagname."); From 506d072657514b794bf809f41160fc81f5336c4a Mon Sep 17 00:00:00 2001 From: Joshua Cole Date: Mon, 6 Feb 2017 18:23:54 -0800 Subject: [PATCH 14/22] Propery crossy parent test --- src/bootstrap.ts | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/src/bootstrap.ts b/src/bootstrap.ts index 8ea8a1f17..44dd79df7 100644 --- a/src/bootstrap.ts +++ b/src/bootstrap.ts @@ -46,28 +46,30 @@ prog [1, "tag", "P"], [1, "name", "Jeff"], - // [2, "tag", "P"], - // [2, "name", "KERY"], + [2, "tag", "P"], + [2, "name", "KERY"], - // [3, "tag", "P"], - // [3, "name", "RAB"], + [3, "tag", "P"], + [3, "name", "RAB"], [4, "tag", "potato"], - // [4, "kind", "idaho"], + [4, "kind", "idaho"], - // [5, "tag", "potato"], - // [5, "kind", "irish gold"], + [5, "tag", "potato"], + [5, "kind", "irish gold"], ]); prog .test(1, [ - [1, "tag", "P", 0, -1] + [1, "tag", "P", 0, -1], + [2, "tag", "P", 0, -1] + ]); -//prog -// .test(2, [ -// //[3, "tag", "P", 0, 1], -// [1, "tag", "P", 0, 1], -// ]); +prog + .test(2, [ + [1, "tag", "P", 0, 1], + ]); + console.log(prog); From 60f76ec2690dd99fc7305094526e720f39fb959c Mon Sep 17 00:00:00 2001 From: Chris Granger Date: Tue, 7 Feb 2017 08:59:43 -0800 Subject: [PATCH 15/22] remove the completely wrong deduping stuff and fix the actual bug we hid with it --- src/runtime/runtime.ts | 64 ++++++++---------------------------------- 1 file changed, 12 insertions(+), 52 deletions(-) diff --git a/src/runtime/runtime.ts b/src/runtime/runtime.ts index 9a17c1041..2b1194be8 100644 --- a/src/runtime/runtime.ts +++ b/src/runtime/runtime.ts @@ -951,9 +951,6 @@ type ResolvedEAVN = {e:ResolvedValue, a:ResolvedValue, v:ResolvedValue, n:Resolv * Base class for nodes, the building blocks of blocks. */ export interface Node { - /** Reset any state associated with this node in-between rounds */ - reset():void; - /** * Evaluate the node in the context of the currently solved prefix, * returning a set of valid prefixes to continue the query as @@ -1022,8 +1019,6 @@ export class JoinNode implements Node { this.proposedResultsArrays = proposedResultsArrays; } - reset() {} - findAffectedConstraints(input:Change, prefix:ID[]):Iterator { // @TODO: Hoist me out. let affectedConstraints = this.affectedConstraints; @@ -1041,7 +1036,7 @@ export class JoinNode implements Node { } applyCombination(index:Index, input:Change, prefix:ID[], transaction:number, round:number, results:Iterator) { - debug(" Join combo:", prefix.toString()); + debug(" Join combo:", prefix.slice()); let countOfSolved = 0; for(let field of prefix) { if(field !== undefined) countOfSolved++; @@ -1225,6 +1220,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; @@ -1234,13 +1231,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(); @@ -1256,7 +1258,6 @@ export class JoinNode implements Node { export class InsertNode implements Node { intermediates:{[key:string]: number|undefined} = {}; - deduper:{[key:number]: ID[][]} = {}; constructor(public e:ID|Register, public a:ID|Register, @@ -1267,51 +1268,11 @@ export class InsertNode implements Node { resolve = Scan.prototype.resolve; - reset() { - this.deduper = {}; - } - - prefixKey(prefix:ID[]) { - let key = 0; - for(let ix = 0, len = prefix.length; ix < len; ix++) { - let value = +prefix[ix]; - key += value; - } - if(isNaN(key)) throw new Error("Got prefix in InsertNode that has an undefined in it"); - return key; - } - - dedupe(prefix:ID[]) { - let {deduper} = this; - let prefixKey = this.prefixKey(prefix); - let matched = false; - let found = deduper[prefixKey]; - if(found) { - maybePrefixLoop: for(let maybePrefix of found) { - if(maybePrefix.length !== prefix.length) continue; - for(let ix = 0, len = prefix.length; ix < len; ix++) { - if(maybePrefix[ix] != prefix[ix]) continue maybePrefixLoop; - } - matched = true; - break; - } - } - if(!matched) { - if(!found) deduper[prefixKey] = [prefix]; - else found.push(prefix); - return true; - } - return false; - } - 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 { - if(!this.dedupe(prefix)) { - return true; - } let {e,a,v,n} = this.resolve(prefix); // @FIXME: This is pretty wasteful to copy one by one here. @@ -1333,6 +1294,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 @@ -1404,8 +1367,6 @@ abstract class BinaryFlow implements Node { constructor(public left:Node, public right:Node) { } - reset() {} - exec(index:Index, input:Change, prefix:ID[], transaction:number, round:number, results:Iterator, changes:Transaction):boolean { let {left, right, leftResults, rightResults} = this; leftResults.clear(); @@ -1612,7 +1573,6 @@ export class Block { // We populate the prefix with values from the input change so we only derive the // results affected by it. for(let node of this.nodes) { - node.reset(); while((prefix = this.results.next()) !== undefined) { let valid = node.exec(index, input, prefix, transaction.transaction, transaction.round, this.nextResults, transaction); if(!valid) { From f204e0fc80f4e81642dfb29d4c2da65a7411bb20 Mon Sep 17 00:00:00 2001 From: Joshua Cole Date: Tue, 7 Feb 2017 11:50:48 -0800 Subject: [PATCH 16/22] Remove extraneous reset stuff --- src/runtime/runtime.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/runtime/runtime.ts b/src/runtime/runtime.ts index 2b1194be8..f5e1a8c5a 100644 --- a/src/runtime/runtime.ts +++ b/src/runtime/runtime.ts @@ -1499,8 +1499,6 @@ export class AntiJoinPresovledRight extends AntiJoin { export class UnionFlow implements Node { constructor(public branches:Node[], public registers:Register[]) { } - reset() {} - exec(index:Index, input:Change, prefix:ID[], transaction:number, round:number, results:Iterator, changes:Transaction):boolean { for(let node of this.branches) { node.exec(index, input, prefix, transaction, round, results, changes); @@ -1527,8 +1525,6 @@ export class ChooseFlow implements Node { } } - reset() {} - exec(index:Index, input:Change, prefix:ID[], transaction:number, round:number, results:Iterator, changes:Transaction):boolean { let {branchResults, branches} = this; let prev:Iterator|undefined; From ccc60a1aa77ee074542ce8798fe919c0fae5a3ad Mon Sep 17 00:00:00 2001 From: Joshua Cole Date: Tue, 7 Feb 2017 15:27:05 -0800 Subject: [PATCH 17/22] Change the exporter to be per block vs per tag --- src/bootstrap.ts | 127 +++++++++++++++++++++++------------------ src/runtime/dsl.ts | 15 ++--- src/runtime/runtime.ts | 96 ++++++++++++++----------------- src/watchers/html.ts | 69 ++++++++++++++-------- 4 files changed, 169 insertions(+), 138 deletions(-) diff --git a/src/bootstrap.ts b/src/bootstrap.ts index 44dd79df7..42220565a 100644 --- a/src/bootstrap.ts +++ b/src/bootstrap.ts @@ -16,60 +16,77 @@ prog ] }); -// prog.test(0, [ -// [2, "tag", "html/element"], -// [2, "tagname", "div"], -// [2, "children", 3], - -// [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], - -// [6, "border", "3px solid green"], - -// [7, "tag", "html/element"], -// [7, "tagname", "span"], -// [7, "text", "meep moop"] -// ]); - -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], - ]); +prog.test(0, [ + [2, "tag", "html/element"], + [2, "tagname", "div"], + [2, "children", 3], + + [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], + + [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)"] +]); + +// 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 942303c26..19f0bb6b3 100644 --- a/src/runtime/dsl.ts +++ b/src/runtime/dsl.ts @@ -361,7 +361,7 @@ class DSLRecord { for(let dslValue of this.__fields[field]) { let value = toValue(dslValue) as (RawValue | Register); if(this.__block.watcher) { - inserts.push(new WatchNode(e, maybeIntern(field), maybeIntern(value), maybeIntern(program.nodeCount++))) + 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++))) } @@ -371,7 +371,7 @@ class DSLRecord { for(let dslValue of dslValues) { let value = toValue(dslValue) as (RawValue | Register); if(this.__block.watcher) { - inserts.push(new WatchNode(e, field, maybeIntern(value), maybeIntern(program.nodeCount++))) + inserts.push(new WatchNode(e, field, maybeIntern(value), maybeIntern(program.nodeCount++), this.__block.__id)) } else { inserts.push(new InsertNode(e, field, maybeIntern(value), maybeIntern(program.nodeCount++))) } @@ -1135,6 +1135,7 @@ export class Program { nodeCount = 0; protected _exporter?:runtime.Exporter; + protected _lastWatch?:number; /** Represents the hierarchy of blocks currently being compiled into runtime nodes. */ contextStack:DSLBlock[] = []; @@ -1144,7 +1145,7 @@ export class Program { } block(name:string, func:BlockFunction) { - let block = new DSLBlock(name, func, this); + let block = new DSLBlock(name, func, this, undefined, undefined); block.prepare(); this.blocks.push(block); this.runtimeBlocks.push(block.block); @@ -1158,13 +1159,13 @@ export class Program { block.prepare(); this.blocks.push(block); this.runtimeBlocks.push(block.block); - + this._lastWatch = block.__id; return this; } - asDiffs(tag:string, handler:runtime.DiffConsumer) { - if(!this._exporter) throw new Error("Must have at least one watch block to export as diffs."); - this._exporter.triggerOnDiffs(GlobalInterner.intern(tag), handler); + 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; } diff --git a/src/runtime/runtime.ts b/src/runtime/runtime.ts index 04e4c9c8d..52ae0b468 100644 --- a/src/runtime/runtime.ts +++ b/src/runtime/runtime.ts @@ -1340,12 +1340,20 @@ 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(change); + transaction.export(this.blockId, change); } } @@ -1618,7 +1626,7 @@ export class Block { export class Transaction { round = 0; protected roundChanges:Change[][] = []; - protected exportedChanges:Change[] = []; + protected exportedChanges:{[blockId:number]: Change[]} = {}; constructor(public transaction:number, public blocks:Block[], public changes:Change[], protected exportHandler?:ExportHandler) {} output(change:Change) { @@ -1628,8 +1636,9 @@ export class Transaction { this.roundChanges[change.round] = cur; } - export(change:Change) { - this.exportedChanges.push(change); + 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 */) { @@ -1695,15 +1704,16 @@ export class Transaction { } } - if(this.exportedChanges.length) { + let exportingBlocks = Object.keys(this.exportedChanges); + if(exportingBlocks.length) { if(!this.exportHandler) throw new Error("Unable to export changes without export handler."); - let exports = createArray("exportsArray"); - this.collapseMultiplicity(this.exportedChanges, exports); - if(exports.length) { - // console.log("Exporting:"); - // console.log(" " + exports.join("\n ")); - this.exportHandler(exports); + + 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 @@ -1716,59 +1726,39 @@ export class Transaction { //------------------------------------------------------------------------------ // Exporter //------------------------------------------------------------------------------ -interface TagMap {[tag:number]: V}; -interface EntityMap {[entityId:number]: V}; -interface AttributeMap {[attributeId:number]: V}; -interface Record {[attribute:string]: RawValue[]}; +interface Map {[key:number]: V}; -type ExportHandler = (changes:Change[]) => void; +type ExportHandler = (blockChanges:Map) => void; export type DiffConsumer = (changes:Readonly) => void; -const TAG = GlobalInterner.intern("tag"); - export class Exporter { - protected _diffTriggers:TagMap = {}; - protected _tags:ID[] = []; + protected _diffTriggers:Map = {}; + protected _blocks:ID[] = []; - triggerOnDiffs(tag:ID, handler:DiffConsumer):void { - if(!this._diffTriggers[tag]) this._diffTriggers[tag] = createArray(); - if(this._diffTriggers[tag].indexOf(handler) === -1) { - this._diffTriggers[tag].push(handler); + 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._tags.indexOf(tag) === -1) { - this._tags.push(tag); + if(this._blocks.indexOf(blockId) === -1) { + this._blocks.push(blockId); } } - handle = (changes:Change[]) => { - let newByTags:TagMap = {}; - let entityChanges:EntityMap = {}; - for(let change of changes) { - if(change.a === TAG) { - if(!newByTags[change.v]) newByTags[change.v] = createArray("exporterNewTags"); - newByTags[change.v].push(change.e); - } - - if(!entityChanges[change.e]) entityChanges[change.e] = createArray("exporterEntityChanges"); - entityChanges[change.e].push(change); - } - - // @NOTE: We're leaving a lot of perf on the table right now. It's not a priority atm. - for(let tag of this._tags) { - if(!newByTags[tag]) continue; - - let entityIds = newByTags[tag]; - if(this._diffTriggers[tag]) { - let output:RawChange[] = createArray("exporterOutput"); - for(let entityId of newByTags[tag]) { - for(let change of entityChanges[entityId]) { - // We'll omit the tag you're listening to so it doesn't pollute your diffs. - if(change.a === TAG && change.v === tag) continue; + 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 this._diffTriggers[tag]) { - trigger(output); + + for(let trigger of diffTriggers) { + trigger(output); + } } } } diff --git a/src/watchers/html.ts b/src/watchers/html.ts index da50295de..28067b5e9 100644 --- a/src/watchers/html.ts +++ b/src/watchers/html.ts @@ -25,7 +25,7 @@ function accumulateChangesAs(changes:RawChange[]) { } interface Style extends Map {__size: number} -interface Instance extends HTMLElement {__element?: string} +interface Instance extends HTMLElement {__element?: string, __styles?: string[]} class HTMLWatcher extends Watcher { styles:Map = Object.create(null); @@ -74,6 +74,37 @@ class HTMLWatcher extends Watcher { style.__size += count; } + addStyleInstance(styleId:string, instanceId:string) { + let instance = this.getInstance(instanceId); + let style = this.getStyle(styleId); + for(let prop in style) { + if(prop === "__size") continue; + instance.style[prop as any] = style[prop] as string; + } + if(this.styleToInstances[styleId]) this.styleToInstances[styleId]!.push(instanceId); + else this.styleToInstances[styleId] = [instanceId]; + + if(!instance.__styles) instance.__styles = []; + if(instance.__styles.indexOf(styleId) === -1) instance.__styles.push(styleId); + } + + removeStyleInstance(styleId:string, instanceId:string) { + let instance = this.instances[instanceId]; + if(!instance) return; + instance.removeAttribute("style"); + let ix = instance.__styles!.indexOf(styleId); + instance.__styles!.splice(ix, 1); + + for(let otherStyleId of instance.__styles!) { + console.log("reapplying", otherStyleId); + let style = this.getStyle(otherStyleId); + for(let prop in style) { + if(prop === "__size") continue; + instance.style[prop as any] = style[prop] as string; + } + } + } + setup() { this.program .block("Elements with no parents are roots.", ({find, record, lib, not}) => { @@ -97,10 +128,10 @@ class HTMLWatcher extends Watcher { .watch("Export all instances.", ({find, record}) => { let instance = find("html/instance"); return [ - record("html/instance", {tagname: instance.tagname, element: instance.element, instance}) + record({tagname: instance.tagname, element: instance.element, instance}) ]; }) - .asDiffs("html/instance", (changes) => { + .asDiffs((changes) => { // console.log("Diffs: (html/instance)"); // console.log(" " + changes.join("\n ")); @@ -119,10 +150,10 @@ class HTMLWatcher extends Watcher { .watch("Export roots.", ({find, record}) => { let root = find("html/root"); return [ - record("html/root", {instance: root}) + record({instance: root}) ]; }) - .asDiffs("html/root", (changes) => { + .asDiffs((changes) => { // console.log("Diffs: (html/root)"); // console.log(" " + changes.join("\n ")); @@ -142,10 +173,10 @@ class HTMLWatcher extends Watcher { .watch("Export instance parents.", ({find, record}) => { let instance = find("html/instance"); return [ - record("html/parent", {instance, parent: instance.parent}) + record({instance, parent: instance.parent}) ]; }) - .asDiffs("html/parent", (changes) => { + .asDiffs((changes) => { // console.log("Diffs: (html/parent)"); // console.log(" " + changes.join("\n ")); @@ -177,11 +208,10 @@ class HTMLWatcher extends Watcher { let style = find("html/style"); let {attribute, value} = lookup(style); return [ - style.add("tag", "html/style") - .add(attribute, value) + style.add(attribute, value) ]; }) - .asDiffs("html/style", (changes) => { + .asDiffs((changes) => { // console.log("Diffs: (html/style)"); // console.log(" " + changes.join("\n ")); @@ -213,12 +243,10 @@ class HTMLWatcher extends Watcher { let elem = instance.element; let {attribute, value} = lookup(elem); return [ - instance - .add("tag", "html/attribute") - .add(attribute, value) + instance.add(attribute, value) ]; }) - .asDiffs("html/attribute", (changes) => { + .asDiffs((changes) => { // console.log("Diffs: (html/attribute)"); // console.log(" " + changes.join("\n ")); @@ -229,15 +257,10 @@ class HTMLWatcher extends Watcher { instance.textContent = count > 0 ? v : undefined; } else if(a === "style") { - this.styleToInstances[v] = e; - let style = this.getStyle(v); - for(let prop of Object.keys(style)) { - if(prop === "__size") continue; - if(count > 0) { - instance.style[prop as any] = style[prop] as string; - } else { - throw new Error("@TODO: Implement me!"); - } + if(count > 0) { + this.addStyleInstance(v, e); + } else { + this.removeStyleInstance(v, e); } } else if(a === "tagname") { From d7aed46d5b6f8cc3b0b7db10d4408970fcd15437 Mon Sep 17 00:00:00 2001 From: Joshua Cole Date: Tue, 7 Feb 2017 15:58:11 -0800 Subject: [PATCH 18/22] add() should always push into dynamic fields --- src/runtime/dsl.ts | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/src/runtime/dsl.ts b/src/runtime/dsl.ts index 19f0bb6b3..96a0ff921 100644 --- a/src/runtime/dsl.ts +++ b/src/runtime/dsl.ts @@ -217,7 +217,7 @@ class DSLRecord { __needsId: boolean = true; __fields: {[field:string]: (RawValue|DSLNode)[]}; - __dynamicFields: [DSLVariable, DSLNode[]][] = []; + __dynamicFields: [DSLVariable|string, DSLNode[]][] = []; constructor(public __block:DSLBlock, tags:string[], initialAttributes:any, entityVariable?:DSLVariable) { this.__id = CURRENT_ID++; let fields:any = {tag: tags}; @@ -313,12 +313,7 @@ class DSLRecord { record.__output = true; this.__block.records.push(record); - if(typeof attributeName === "string") { - record.__fields[attributeName] = values; - - } else { - record.__dynamicFields.push([attributeName, values]); - } + record.__dynamicFields.push([attributeName, values]); return this; } @@ -367,13 +362,13 @@ class DSLRecord { } } for(let [dslField, dslValues] of this.__dynamicFields) { - let field = toValue(dslField) as Register; + 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, field, maybeIntern(value), maybeIntern(program.nodeCount++), this.__block.__id)) + inserts.push(new WatchNode(e, maybeIntern(field), maybeIntern(value), maybeIntern(program.nodeCount++), this.__block.__id)) } else { - inserts.push(new InsertNode(e, field, maybeIntern(value), maybeIntern(program.nodeCount++))) + inserts.push(new InsertNode(e, maybeIntern(field), maybeIntern(value), maybeIntern(program.nodeCount++))) } } } From ad36faa8bad913bae850b9b7fabe52f2ef8d5534 Mon Sep 17 00:00:00 2001 From: Joshua Cole Date: Tue, 7 Feb 2017 15:59:57 -0800 Subject: [PATCH 19/22] Clean up cruft --- src/runtime/dsl.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/runtime/dsl.ts b/src/runtime/dsl.ts index 96a0ff921..ae1684712 100644 --- a/src/runtime/dsl.ts +++ b/src/runtime/dsl.ts @@ -1140,7 +1140,7 @@ export class Program { } block(name:string, func:BlockFunction) { - let block = new DSLBlock(name, func, this, undefined, undefined); + let block = new DSLBlock(name, func, this); block.prepare(); this.blocks.push(block); this.runtimeBlocks.push(block.block); @@ -1183,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; } } From 1ec234d14eada2309d628789ccf5fc0e6e7911a4 Mon Sep 17 00:00:00 2001 From: Joshua Cole Date: Tue, 7 Feb 2017 16:01:43 -0800 Subject: [PATCH 20/22] Fix bug in maybeReverse that made 0 -> undefined --- src/runtime/runtime.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/runtime/runtime.ts b/src/runtime/runtime.ts index 52ae0b468..156bf492f 100644 --- a/src/runtime/runtime.ts +++ b/src/runtime/runtime.ts @@ -40,7 +40,7 @@ export function printConstraint(constraint:Constraint) { } export function maybeReverse(value?:ID):ID|RawValue|undefined { - if(!value) return undefined; + if(value === undefined) return value; let raw = GlobalInterner.reverse(value); return (""+raw).indexOf("|") === -1 ? raw : value; } From 7cf13abc000651f13f236206028f16870a1b6f5c Mon Sep 17 00:00:00 2001 From: Joshua Cole Date: Tue, 7 Feb 2017 16:02:58 -0800 Subject: [PATCH 21/22] Remove extraneous block --- src/watchers/html.ts | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/watchers/html.ts b/src/watchers/html.ts index 28067b5e9..49ad8ba2b 100644 --- a/src/watchers/html.ts +++ b/src/watchers/html.ts @@ -96,7 +96,6 @@ class HTMLWatcher extends Watcher { instance.__styles!.splice(ix, 1); for(let otherStyleId of instance.__styles!) { - console.log("reapplying", otherStyleId); let style = this.getStyle(otherStyleId); for(let prop in style) { if(prop === "__size") continue; @@ -197,15 +196,9 @@ class HTMLWatcher extends Watcher { } }) - .block("Records in the style attribute of an element are styles.", ({find, record, lib, not}) => { + .watch("Export html styles.", ({find, record, lib, not, lookup}) => { let elem = find("html/element"); let style = elem.style; - return [ - style.add("tag", "html/style") - ]; - }) - .watch("Export html styles.", ({find, record, lib, not, lookup}) => { - let style = find("html/style"); let {attribute, value} = lookup(style); return [ style.add(attribute, value) From a7988473d82eaae633b85b4a57c405252f0942b1 Mon Sep 17 00:00:00 2001 From: Joshua Cole Date: Tue, 7 Feb 2017 16:12:31 -0800 Subject: [PATCH 22/22] Add sorted insertion --- src/bootstrap.ts | 7 +++++-- src/watchers/html.ts | 25 +++++++++++++++++++++++-- 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/src/bootstrap.ts b/src/bootstrap.ts index 42220565a..c10ac2197 100644 --- a/src/bootstrap.ts +++ b/src/bootstrap.ts @@ -20,6 +20,7 @@ prog.test(0, [ [2, "tag", "html/element"], [2, "tagname", "div"], [2, "children", 3], + [2, "sort", 1], [3, "tag", "html/element"], [3, "tagname", "span"], @@ -33,12 +34,13 @@ prog.test(0, [ [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"] + [7, "text", "meep moop"], ]); prog.test(1, [ @@ -55,7 +57,8 @@ prog.test(3, [ [8, "tag", "html/element"], [8, "tagname", "div"], [8, "style", 4], - [8, "text", "Jeff (from accounting)"] + [8, "text", "Jeff (from accounting)"], + [8, "sort", 0] ]); // prog diff --git a/src/watchers/html.ts b/src/watchers/html.ts index 49ad8ba2b..575f50b77 100644 --- a/src/watchers/html.ts +++ b/src/watchers/html.ts @@ -25,7 +25,7 @@ function accumulateChangesAs(changes:RawChange[]) { } interface Style extends Map {__size: number} -interface Instance extends HTMLElement {__element?: string, __styles?: string[]} +interface Instance extends HTMLElement {__element?: string, __styles?: string[], __sort?: number} class HTMLWatcher extends Watcher { styles:Map = Object.create(null); @@ -59,7 +59,21 @@ class HTMLWatcher extends Watcher { } insertChild(parent:Instance, child:Instance) { - parent.appendChild(child); + let current; + for(let curIx = 0; curIx < parent.childNodes.length; curIx++) { + let cur = parent.childNodes[curIx] as Instance; + if(cur === child) continue; + if(cur.__sort !== undefined && cur.__sort > child.__sort) { + current = cur; + break; + } + } + + if(current) { + parent.insertBefore(child, current); + } else { + parent.appendChild(child); + } } setStyleAttribute(style:Style, attribute:string, value:RawValue, count:-1|1) { @@ -266,6 +280,13 @@ class HTMLWatcher extends Watcher { } else if(a === "children") { // Handled by html/parent + } else if(a === "sort") { + instance.__sort = v; + let parent = instance.parentElement; + if(parent) { + this.insertChild(parent, instance); + } + } else { if(count === 1) { instance.setAttribute(a, v);